commit 0f855c5ac2dccbc1c1df99b9d5ee17d6293d82df Author: Thomas Date: Wed Apr 27 15:20:42 2022 +0200 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..050d01186 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +*.iml +.gradle +/local.properties +/.idea/* +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 000000000..302fde2a1 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,122 @@ +plugins { + id 'com.android.application' +} +def flavor +android { + compileSdk 31 + + defaultConfig { + minSdk 21 + targetSdk 31 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + flavorDimensions "default" + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + productFlavors { + fdroid { + applicationId "fr.gouv.etalab.mastodon" + buildConfigField "boolean", "DONATIONS", "true" + buildConfigField "boolean", "push", "false" + flavor = "fdroid" + } + playstore { + applicationId "app.fedilab.android" + buildConfigField "boolean", "DONATIONS", "false" + buildConfigField "boolean", "push", "true" + flavor = "playstore" + } + } + lintOptions { + checkReleaseBuilds false + abortOnError false + } + buildFeatures { + viewBinding true + } + sourceSets { + playstore { + manifest.srcFile "src/playstore/AndroidManifest.xml" + java.srcDirs = ['src/main/java', 'src/playstore/java'] + res.srcDirs = ['src/main/res', 'src/playstore/res'] + } + fdroid { + java.srcDirs = ['src/main/java', 'src/fdroid/java'] + res.srcDirs = ['src/main/res', 'src/fdroid/res'] + } + } + configurations { + all { + exclude group: 'androidx.lifecycle', module: 'lifecycle-viewmodel-ktx' + } + } +} +allprojects { + repositories { + mavenLocal() + maven { url "https://jitpack.io" } + } +} +dependencies { + implementation project(':autoimageslider') + implementation 'androidx.appcompat:appcompat:1.4.1' + implementation 'com.google.android.material:material:1.5.0' + implementation 'androidx.constraintlayout:constraintlayout:2.1.3' + implementation "com.google.code.gson:gson:2.8.6" + implementation 'com.squareup.retrofit2:retrofit:2.9.0' + implementation 'com.squareup.retrofit2:converter-gson:2.9.0' + implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' + implementation 'androidx.preference:preference:1.2.0' + implementation 'com.github.evozi:Cyanea:1.0.7' + implementation 'com.vanniktech:emoji-one:0.6.0' + implementation 'com.github.GrenderG:Toasty:1.5.2' + implementation 'org.framagit.tom79:SparkButton:1.0.13' + implementation "com.github.bumptech.glide:glide:4.12.0" + implementation 'com.github.mergehez:ArgPlayer:v3.1' + implementation ("com.github.bumptech.glide:recyclerview-integration:4.12.0") { + // Excludes the support library because it's already included by Glide. + transitive = false + } + implementation project(path: ':mytransl') + implementation project(path: ':ratethisapp') + annotationProcessor "com.github.bumptech.glide:compiler:4.12.0" + implementation 'jp.wasabeef:glide-transformations:4.3.0' + implementation 'com.github.penfeizhou.android.animation:apng:2.17.0' + implementation 'com.github.penfeizhou.android.animation:gif:2.17.0' + implementation 'com.google.android.exoplayer:exoplayer:2.16.1' + implementation 'com.github.piasy:rxandroidaudio:1.7.0' + implementation 'com.github.piasy:AudioProcessor:1.7.0' + implementation "androidx.work:work-runtime:2.7.1" + implementation 'app.futured.hauler:hauler:5.0.0' + implementation "com.github.chrisbanes:PhotoView:2.3.0" + implementation "ch.acra:acra-mail:5.5.0" + implementation "ch.acra:acra-limiter:5.5.0" + implementation "ch.acra:acra-notification:5.5.0" + + implementation "com.madgag.spongycastle:bctls-jdk15on:1.58.0.0" + implementation 'com.github.UnifiedPush:android-connector:2.0.0' + // implementation 'com.github.UnifiedPush:android-foss_embedded_fcm_distributor:1.0.0-beta1' + playstoreImplementation 'com.github.UnifiedPush:android-embedded_fcm_distributor:1.1.0' + implementation 'com.burhanrashid52:photoeditor:1.5.1' + implementation 'androidx.multidex:multidex:2.0.1' + implementation 'com.github.PhilJay:MPAndroidChart:v3.1.0' + implementation 'androidx.lifecycle:lifecycle-livedata:2.4.1' + implementation 'androidx.lifecycle:lifecycle-viewmodel:2.4.1' + implementation 'androidx.navigation:navigation-fragment:2.4.1' + implementation 'androidx.navigation:navigation-ui:2.4.1' + testImplementation 'junit:junit:' + androidTestImplementation 'androidx.test.ext:junit:1.1.3' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.8.1' +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/androidTest/java/app/fedilab/android/ExampleInstrumentedTest.java b/app/src/androidTest/java/app/fedilab/android/ExampleInstrumentedTest.java new file mode 100644 index 000000000..f422a923b --- /dev/null +++ b/app/src/androidTest/java/app/fedilab/android/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package app.fedilab.android; + +import android.content.Context; + +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + assertEquals("app.fedilab.android", appContext.getPackageName()); + } +} \ No newline at end of file diff --git a/app/src/fdroid/java/app/fedilab/android/activities/MainActivity.java b/app/src/fdroid/java/app/fedilab/android/activities/MainActivity.java new file mode 100644 index 000000000..aed6e66e9 --- /dev/null +++ b/app/src/fdroid/java/app/fedilab/android/activities/MainActivity.java @@ -0,0 +1,27 @@ +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ +package app.fedilab.android.activities; + +import app.fedilab.android.BaseMainActivity; + +public class MainActivity extends BaseMainActivity { + + + @Override + protected void rateThisApp() { + // do nothing + } + +} diff --git a/app/src/fdroid/res/xml/file_paths.xml b/app/src/fdroid/res/xml/file_paths.xml new file mode 100644 index 000000000..758bbce98 --- /dev/null +++ b/app/src/fdroid/res/xml/file_paths.xml @@ -0,0 +1,11 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000..1cd7450f2 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,187 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/assets/themes/contributors/Fedilab_theme_Breeze_Dark_Yellow.csv b/app/src/main/assets/themes/contributors/Fedilab_theme_Breeze_Dark_Yellow.csv new file mode 100644 index 000000000..3eec93e44 --- /dev/null +++ b/app/src/main/assets/themes/contributors/Fedilab_theme_Breeze_Dark_Yellow.csv @@ -0,0 +1,13 @@ +base_theme,2 +author,Fedilab +name,Breeze Dark - Yellow +theme_boost_header_color,-14012878 +theme_statuses_color,-14473687 +theme_link_color,-12734743 +theme_icons_color,-4340793 +pref_color_background,-15658735 +pref_color_navigation_bar,true +pref_color_status_bar,true +theme_accent,-148405 +theme_text_color,-1052431 +theme_primary,-13552069 diff --git a/app/src/main/assets/themes/contributors/Fedilab_theme_Cyberpunk_Neon.csv b/app/src/main/assets/themes/contributors/Fedilab_theme_Cyberpunk_Neon.csv new file mode 100644 index 000000000..7fb99a5d2 --- /dev/null +++ b/app/src/main/assets/themes/contributors/Fedilab_theme_Cyberpunk_Neon.csv @@ -0,0 +1,15 @@ +base_theme,2 +author,Roboron +name,Cyberpunk Neon +theme_boost_header_color,-16776697, +theme_text_header_1_line,-1441575, +theme_text_header_2_line,-5242717, +theme_statuses_color,-16181197, +theme_link_color,-1441575, +theme_icons_color,-16138810, +pref_color_background,-16774370, +pref_color_navigation_bar,true, +pref_color_status_bar,true, +theme_accent,-1441575, +theme_text_color,-16138810, +theme_primary,-16774370, diff --git a/app/src/main/assets/themes/contributors/Fedilab_theme_Grey_Orange.csv b/app/src/main/assets/themes/contributors/Fedilab_theme_Grey_Orange.csv new file mode 100644 index 000000000..2c5c107ce --- /dev/null +++ b/app/src/main/assets/themes/contributors/Fedilab_theme_Grey_Orange.csv @@ -0,0 +1,15 @@ +base_theme,2 +author,Jøta Seth +name,Grey Orange +theme_boost_header_color,-14869219 +theme_text_header_1_line,-1 +theme_text_header_2_line,-1 +theme_statuses_color,-14145496 +theme_link_color,-26624 +theme_icons_color,-26624 +pref_color_background,-13092808 +pref_color_navigation_bar,true +pref_color_status_bar,true +theme_accent,-26624 +theme_text_color,-1 +theme_primary,-14408668 diff --git a/app/src/main/assets/themes/contributors/Fedilab_theme_Gruvbox_OLED.csv b/app/src/main/assets/themes/contributors/Fedilab_theme_Gruvbox_OLED.csv new file mode 100644 index 000000000..d03b3b812 --- /dev/null +++ b/app/src/main/assets/themes/contributors/Fedilab_theme_Gruvbox_OLED.csv @@ -0,0 +1,15 @@ +base_theme,2 +author,@AntoineD@h.kher.nl +name,Gruvbox OLED +theme_boost_header_color,-16777216 +theme_text_header_1_line,-265785 +theme_text_header_2_line,-6777062 +theme_statuses_color,-16777216 +theme_link_color,-2647775 +theme_icons_color,-7175308 +pref_color_background,-16777216 +pref_color_navigation_bar,true +pref_color_status_bar,true +theme_accent,-9921174 +theme_text_color,-265785 +theme_primary,-16777216 diff --git a/app/src/main/assets/themes/contributors/Fedilab_theme_Less_Angry_Orange.csv b/app/src/main/assets/themes/contributors/Fedilab_theme_Less_Angry_Orange.csv new file mode 100644 index 000000000..26c89789a --- /dev/null +++ b/app/src/main/assets/themes/contributors/Fedilab_theme_Less_Angry_Orange.csv @@ -0,0 +1,15 @@ +base_theme,2 +author,AngryTux +name,Less Angry Orange +theme_boost_header_color,-15855063 +theme_text_header_1_line,-2128640 +theme_text_header_2_line,-5329234 +theme_statuses_color,-1 +theme_link_color,-12146699 +theme_icons_color,-2128640 +pref_color_background,-15987700 +pref_color_navigation_bar,true +pref_color_status_bar,true +theme_accent,-3968000 +theme_text_color,-197380 +theme_primary,-14408668 diff --git a/app/src/main/assets/themes/contributors/Fedilab_theme_Mondstern_Fedilab.csv b/app/src/main/assets/themes/contributors/Fedilab_theme_Mondstern_Fedilab.csv new file mode 100644 index 000000000..b9b23d8f6 --- /dev/null +++ b/app/src/main/assets/themes/contributors/Fedilab_theme_Mondstern_Fedilab.csv @@ -0,0 +1,15 @@ +base_theme,2 +author,Mondstern +name,Mondstern Fedilab +theme_boost_header_color,-1, +theme_text_header_1_line,-13855804, +theme_text_header_2_line,-16227945, +theme_statuses_color,-14935012, +theme_link_color,-15542685, +theme_icons_color,-10723999, +pref_color_background,-15921907, +pref_color_navigation_bar,false, +pref_color_status_bar,false, +theme_accent,-15542685, +theme_text_color,-1, +theme_primary,-14474461, diff --git a/app/src/main/assets/themes/contributors/Fedilab_theme_Nocturnal.csv b/app/src/main/assets/themes/contributors/Fedilab_theme_Nocturnal.csv new file mode 100644 index 000000000..ed4424495 --- /dev/null +++ b/app/src/main/assets/themes/contributors/Fedilab_theme_Nocturnal.csv @@ -0,0 +1,13 @@ +base_theme,2 +author,Fedilab +name,Nocturnal +theme_boost_header_color,-12895429 +theme_statuses_color,-13553359 +theme_link_color,-16747570 +theme_icons_color,-10158118 +pref_color_background,-14606047 +pref_color_navigation_bar,true +pref_color_status_bar,true +theme_accent,-13136013 +theme_text_color,-2236963 +theme_primary,-14013910 diff --git a/app/src/main/assets/themes/contributors/Fedilab_theme_Photon_Dark.csv b/app/src/main/assets/themes/contributors/Fedilab_theme_Photon_Dark.csv new file mode 100644 index 000000000..3284a4e4d --- /dev/null +++ b/app/src/main/assets/themes/contributors/Fedilab_theme_Photon_Dark.csv @@ -0,0 +1,15 @@ +base_theme,2 +author,Jøta Seth +name,Photon Dark +theme_boost_header_color,-14145496 +theme_text_header_1_line,-1 +theme_text_header_2_line,-1 +theme_statuses_color,-14935012 +theme_link_color,-14059009 +theme_icons_color,-9474193 +pref_color_background,-15921907 +pref_color_navigation_bar,true +pref_color_status_bar,true +theme_accent,-14059009 +theme_text_color,-1 +theme_primary,-14474461 diff --git a/app/src/main/assets/themes/contributors/Fedilab_theme_Solarized_Dark_Purple.csv b/app/src/main/assets/themes/contributors/Fedilab_theme_Solarized_Dark_Purple.csv new file mode 100644 index 000000000..6c4343274 --- /dev/null +++ b/app/src/main/assets/themes/contributors/Fedilab_theme_Solarized_Dark_Purple.csv @@ -0,0 +1,15 @@ +base_theme,2 +author,Fedilab +name,Solarized Dark - Purple +theme_boost_header_color,-16506327 +theme_text_header_1_line,-1120043 +theme_text_header_2_line,-1120043 +theme_statuses_color,-16304574 +theme_link_color,-14251054 +theme_icons_color,-7102047 +pref_color_background,-16766154 +pref_color_navigation_bar,true +pref_color_status_bar,true +theme_accent,-9670204 +theme_text_color,-133405 +theme_primary,-16304574 diff --git a/app/src/main/assets/themes/cyanea_themes.json b/app/src/main/assets/themes/cyanea_themes.json new file mode 100644 index 000000000..db8eff2b1 --- /dev/null +++ b/app/src/main/assets/themes/cyanea_themes.json @@ -0,0 +1,47 @@ +[ + { + "theme_name": "Dark", + "base_theme": "DARK", + "primary": "#FF272727", + "primary_dark": "#FF272727", + "primary_light": "#FFd9e1e8", + "accent": "#FF2b90d9", + "accent_dark": "#FF1b80c9", + "accent_light": "#FF772b90d9", + "background": "#FF121212", + "background_dark": "#FF282c37", + "background_light": "#FF282c37", + "should_tint_statusbar": true, + "should_tint_navbar": true + }, + { + "theme_name": "Light", + "base_theme": "LIGHT", + "primary": "#FFFFFF", + "primary_dark": "#FFFFFFFF", + "primary_light": "#FFd9e1e8", + "accent": "#FF2b90d9", + "accent_dark": "#FF1b80c9", + "accent_light": "#FF772b90d9", + "background": "#FFFFFFFF", + "background_dark": "#FFFFFFFF", + "background_light": "#FFFFFFFF", + "should_tint_statusbar": true, + "should_tint_navbar": true + }, + { + "theme_name": "Black", + "base_theme": "DARK", + "primary": "#FF000000", + "primary_dark": "#FF000000", + "primary_light": "#FF000000", + "accent": "#FF606984", + "accent_dark": "#FF606984", + "accent_light": "#FF606984", + "background": "#FF000000", + "background_dark": "#FF000000", + "background_light": "#FF000000", + "should_tint_statusbar": true, + "should_tint_navbar": true + } +] \ No newline at end of file diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png new file mode 100644 index 000000000..e1d38cb83 Binary files /dev/null and b/app/src/main/ic_launcher-playstore.png differ diff --git a/app/src/main/java/app/fedilab/android/BaseMainActivity.java b/app/src/main/java/app/fedilab/android/BaseMainActivity.java new file mode 100644 index 000000000..1e13850a8 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/BaseMainActivity.java @@ -0,0 +1,677 @@ +package app.fedilab.android; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import static app.fedilab.android.BaseMainActivity.status.DISCONNECTED; +import static app.fedilab.android.BaseMainActivity.status.UNKNOWN; +import static app.fedilab.android.helper.Helper.PREF_USER_TOKEN; +import static app.fedilab.android.helper.Helper.deleteDir; + +import android.annotation.SuppressLint; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.graphics.PorterDuff; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.view.ContextThemeWrapper; +import android.view.Menu; +import android.view.MenuItem; +import android.view.SubMenu; +import android.view.View; +import android.view.inputmethod.InputMethodManager; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.widget.PopupMenu; +import androidx.appcompat.widget.SearchView; +import androidx.core.app.ActivityOptionsCompat; +import androidx.core.view.GravityCompat; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; +import androidx.navigation.NavController; +import androidx.navigation.Navigation; +import androidx.navigation.ui.AppBarConfiguration; +import androidx.navigation.ui.NavigationUI; +import androidx.preference.PreferenceManager; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.load.resource.gif.GifDrawable; +import com.bumptech.glide.request.target.CustomTarget; +import com.bumptech.glide.request.transition.Transition; +import com.jaredrummler.cyanea.Cyanea; + +import java.io.File; +import java.lang.ref.WeakReference; +import java.util.HashMap; +import java.util.List; + +import app.fedilab.android.activities.ActionActivity; +import app.fedilab.android.activities.BaseActivity; +import app.fedilab.android.activities.ComposeActivity; +import app.fedilab.android.activities.DraftActivity; +import app.fedilab.android.activities.FilterActivity; +import app.fedilab.android.activities.InstanceActivity; +import app.fedilab.android.activities.InstanceHealthActivity; +import app.fedilab.android.activities.LoginActivity; +import app.fedilab.android.activities.MainActivity; +import app.fedilab.android.activities.MastodonListActivity; +import app.fedilab.android.activities.ProfileActivity; +import app.fedilab.android.activities.ProxyActivity; +import app.fedilab.android.activities.ReorderTimelinesActivity; +import app.fedilab.android.activities.ScheduledActivity; +import app.fedilab.android.activities.SearchResultTabActivity; +import app.fedilab.android.activities.SettingsActivity; +import app.fedilab.android.broadcastreceiver.NetworkStateReceiver; +import app.fedilab.android.client.entities.Account; +import app.fedilab.android.client.entities.Pinned; +import app.fedilab.android.client.entities.app.PinnedTimeline; +import app.fedilab.android.client.mastodon.entities.Filter; +import app.fedilab.android.client.mastodon.entities.Instance; +import app.fedilab.android.client.mastodon.entities.MastodonList; +import app.fedilab.android.databinding.ActivityMainBinding; +import app.fedilab.android.databinding.NavHeaderMainBinding; +import app.fedilab.android.exception.DBException; +import app.fedilab.android.helper.Helper; +import app.fedilab.android.helper.PinnedTimelineHelper; +import app.fedilab.android.helper.PushHelper; +import app.fedilab.android.helper.ThemeHelper; +import app.fedilab.android.ui.fragment.timeline.FragmentMastodonTimeline; +import app.fedilab.android.viewmodel.mastodon.AccountsVM; +import app.fedilab.android.viewmodel.mastodon.InstancesVM; +import app.fedilab.android.viewmodel.mastodon.TimelinesVM; +import app.fedilab.android.viewmodel.mastodon.TopBarVM; +import es.dmoral.toasty.Toasty; + +public abstract class BaseMainActivity extends BaseActivity implements NetworkStateReceiver.NetworkStateReceiverListener { + + public static String currentInstance, currentToken, currentUserID, client_id, client_secret, software; + public static Account.API api; + public static boolean admin; + public static WeakReference accountWeakReference; + public static HashMap mPageReferenceMap; + public static status networkAvailable = UNKNOWN; + public static Instance instanceInfo; + public static List mainFilters; + public static boolean filterFetched; + Fragment currentFragment; + private Account account; + private AppBarConfiguration mAppBarConfiguration; + private ActivityMainBinding binding; + private Pinned pinned; + + private final BroadcastReceiver broadcast_data = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + Bundle b = intent.getExtras(); + if (b != null) { + if (b.getBoolean(Helper.RECEIVE_REDRAW_TOPBAR, false)) { + List mastodonLists = (List) b.getSerializable(Helper.RECEIVE_MASTODON_LIST); + redrawPinned(mastodonLists); + } else if (b.getBoolean(Helper.RECEIVE_RECREATE_ACTIVITY, false)) { + Cyanea.getInstance().edit().apply().recreate(BaseMainActivity.this); + } + } + } + }; + private NetworkStateReceiver networkStateReceiver; + private boolean headerMenuOpen; + + protected abstract void rateThisApp(); + + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + mamageNewIntent(intent); + } + + @SuppressLint("ApplySharedPref") + private void mamageNewIntent(Intent intent) { + if (intent == null) + return; + String action = intent.getAction(); + String type = intent.getType(); + Bundle extras = intent.getExtras(); + String userIdIntent, instanceIntent; + if (extras != null && extras.containsKey(Helper.INTENT_ACTION)) { + userIdIntent = extras.getString(Helper.PREF_KEY_ID); //Id of the account in the intent + instanceIntent = extras.getString(Helper.PREF_INSTANCE); + if (extras.getInt(Helper.INTENT_ACTION) == Helper.NOTIFICATION_INTENT) { + try { + Account account = new Account(BaseMainActivity.this).getUniqAccount(userIdIntent, instanceIntent); + SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(BaseMainActivity.this); + headerMenuOpen = false; + Toasty.info(BaseMainActivity.this, getString(R.string.toast_account_changed, "@" + account.mastodon_account.acct + "@" + account.instance), Toasty.LENGTH_LONG).show(); + BaseMainActivity.currentToken = account.token; + BaseMainActivity.currentUserID = account.user_id; + api = account.api; + SharedPreferences.Editor editor = sharedpreferences.edit(); + editor.putString(PREF_USER_TOKEN, account.token); + editor.commit(); + Intent mainActivity = new Intent(this, MainActivity.class); + startActivity(mainActivity); + intent.removeExtra(Helper.INTENT_ACTION); + finish(); + } catch (DBException e) { + e.printStackTrace(); + } + } + } + + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(BaseMainActivity.this); + ThemeHelper.applyTheme(this); + if (!Helper.isLoggedIn(BaseMainActivity.this)) { + //It is not, the user is redirected to the login page + Intent myIntent = new Intent(BaseMainActivity.this, LoginActivity.class); + startActivity(myIntent); + finish(); + return; + } else { + BaseMainActivity.currentToken = sharedpreferences.getString(Helper.PREF_USER_TOKEN, null); + } + mamageNewIntent(getIntent()); + ThemeHelper.initiliazeColors(BaseMainActivity.this); + filterFetched = false; + networkStateReceiver = new NetworkStateReceiver(); + networkStateReceiver.addListener(this); + binding = ActivityMainBinding.inflate(getLayoutInflater()); + setContentView(binding.getRoot()); + setSupportActionBar(binding.toolbar); + ActionBar actionBar = getSupportActionBar(); + //Remove title + if (actionBar != null) { + actionBar.setDisplayShowTitleEnabled(false); + } + rateThisApp(); + SharedPreferences cyneaPref = getSharedPreferences("com.jaredrummler.cyanea", Context.MODE_PRIVATE); + binding.tabLayout.setTabTextColors(ThemeHelper.getAttColor(BaseMainActivity.this, R.attr.mTextColor), cyneaPref.getInt("theme_accent", -1)); + binding.tabLayout.setTabIconTint(ThemeHelper.getColorStateList(BaseMainActivity.this)); + binding.compose.setOnClickListener(v -> startActivity(new Intent(this, ComposeActivity.class))); + headerMenuOpen = false; + binding.bottomNavView.inflateMenu(R.menu.bottom_nav_menu); + binding.bottomNavView.setOnItemSelectedListener(item -> { + int itemId = item.getItemId(); + if (itemId == R.id.nav_home) { + binding.viewPager.setCurrentItem(0); + } else if (itemId == R.id.nav_local) { + binding.viewPager.setCurrentItem(1); + } else if (itemId == R.id.nav_public) { + binding.viewPager.setCurrentItem(2); + } else if (itemId == R.id.nav_notifications) { + binding.viewPager.setCurrentItem(3); + } else if (itemId == R.id.nav_privates) { + binding.viewPager.setCurrentItem(4); + } + return true; + }); + + + // Passing each menu ID as a set of Ids because each + // menu should be considered as top level destinations. + mAppBarConfiguration = new AppBarConfiguration.Builder() + .setOpenableLayout(binding.drawerLayout) + .build(); + + NavHeaderMainBinding headerMainBinding = NavHeaderMainBinding.inflate(getLayoutInflater()); + binding.navView.addHeaderView(headerMainBinding.getRoot()); + binding.navView.setNavigationItemSelectedListener(menuItem -> { + int id = menuItem.getItemId(); + if (id == R.id.nav_drafts) { + Intent intent = new Intent(this, DraftActivity.class); + startActivity(intent); + } else if (id == R.id.nav_reorder) { + Intent intent = new Intent(this, ReorderTimelinesActivity.class); + startActivity(intent); + } else if (id == R.id.nav_interactions) { + Intent intent = new Intent(this, ActionActivity.class); + startActivity(intent); + } else if (id == R.id.nav_filter) { + Intent intent = new Intent(this, FilterActivity.class); + startActivity(intent); + } else if (id == R.id.nav_list) { + Intent intent = new Intent(this, MastodonListActivity.class); + startActivity(intent); + } else if (id == R.id.nav_settings) { + Intent intent = new Intent(this, SettingsActivity.class); + startActivity(intent); + } else if (id == R.id.nav_scheduled) { + Intent intent = new Intent(this, ScheduledActivity.class); + startActivity(intent); + } + binding.drawerLayout.close(); + return false; + }); + headerMainBinding.instanceInfoContainer.setOnClickListener(v -> startActivity(new Intent(BaseMainActivity.this, InstanceHealthActivity.class))); + headerMainBinding.accountProfilePicture.setOnClickListener(v -> { + Intent intent = new Intent(BaseMainActivity.this, ProfileActivity.class); + Bundle b = new Bundle(); + b.putSerializable(Helper.ARG_ACCOUNT, account.mastodon_account); + intent.putExtras(b); + ActivityOptionsCompat options = ActivityOptionsCompat + .makeSceneTransitionAnimation(BaseMainActivity.this, headerMainBinding.instanceInfoContainer, getString(R.string.activity_porfile_pp)); + startActivity(intent, options.toBundle()); + }); + headerMainBinding.changeAccount.setOnClickListener(v -> { + headerMenuOpen = !headerMenuOpen; + if (headerMenuOpen) { + headerMainBinding.ownerAccounts.setImageResource(R.drawable.ic_baseline_arrow_drop_up_24); + new Thread(() -> { + try { + List accounts = new Account(BaseMainActivity.this).getAll(); + Handler mainHandler = new Handler(Looper.getMainLooper()); + Runnable myRunnable = () -> { + binding.navView.getMenu().clear(); + binding.navView.inflateMenu(R.menu.menu_accounts); + headerMenuOpen = true; + + Menu mainMenu = binding.navView.getMenu(); + SubMenu currentSubmenu = null; + String lastInstance = ""; + if (accounts != null) { + for (final Account account : accounts) { + if (!currentToken.equalsIgnoreCase(account.token)) { + if (!lastInstance.trim().equalsIgnoreCase(account.instance.trim())) { + lastInstance = account.instance.toUpperCase(); + currentSubmenu = mainMenu.addSubMenu(account.instance.toUpperCase()); + } + if (currentSubmenu == null) { + continue; + } + final MenuItem item = currentSubmenu.add("@" + account.mastodon_account.acct); + item.setIcon(R.drawable.ic_person); + boolean disableGif = sharedpreferences.getBoolean(getString(R.string.SET_DISABLE_GIF), false); + String url = !disableGif ? account.mastodon_account.avatar : account.mastodon_account.avatar_static; + if (url.startsWith("/")) { + url = "https://" + account.instance + account.mastodon_account.avatar; + } + if (!this.isDestroyed() && !this.isFinishing()) { + if (url.contains(".gif")) { + Glide.with(BaseMainActivity.this) + .asGif() + .load(url) + .into(new CustomTarget() { + @Override + public void onResourceReady(@NonNull GifDrawable resource, Transition transition) { + item.setIcon(resource); + item.getIcon().setColorFilter(0xFFFFFFFF, PorterDuff.Mode.MULTIPLY); + } + + @Override + public void onLoadCleared(@Nullable Drawable placeholder) { + + } + }); + } else { + Glide.with(BaseMainActivity.this) + .asDrawable() + .load(url) + .into(new CustomTarget() { + @Override + public void onResourceReady(@NonNull Drawable resource, Transition transition) { + item.setIcon(resource); + item.getIcon().setColorFilter(0xFFFFFFFF, PorterDuff.Mode.MULTIPLY); + } + + @Override + public void onLoadCleared(@Nullable Drawable placeholder) { + + } + }); + } + + } + item.setOnMenuItemClickListener(item1 -> { + if (!this.isFinishing()) { + headerMenuOpen = false; + Toasty.info(BaseMainActivity.this, getString(R.string.toast_account_changed, "@" + account.mastodon_account.acct + "@" + account.instance), Toasty.LENGTH_LONG).show(); + BaseMainActivity.currentToken = account.token; + BaseMainActivity.currentUserID = account.user_id; + api = account.api; + SharedPreferences.Editor editor = sharedpreferences.edit(); + editor.putString(PREF_USER_TOKEN, account.token); + editor.commit(); + //The user is now aut + //The user is now authenticated, it will be redirected to MainActivity + Intent mainActivity = new Intent(this, MainActivity.class); + startActivity(mainActivity); + finish(); + headerMainBinding.ownerAccounts.setImageResource(R.drawable.ic_baseline_arrow_drop_down_24); + return true; + } + return false; + }); + + } + } + + } + currentSubmenu = mainMenu.addSubMenu(""); + MenuItem addItem = currentSubmenu.add(R.string.add_account); + addItem.setIcon(R.drawable.ic_baseline_person_add_24); + addItem.setOnMenuItemClickListener(item -> { + Intent intent = new Intent(BaseMainActivity.this, LoginActivity.class); + startActivity(intent); + return true; + }); + + }; + mainHandler.post(myRunnable); + } catch (DBException e) { + e.printStackTrace(); + } + }).start(); + } else { + binding.navView.getMenu().clear(); + binding.navView.inflateMenu(R.menu.activity_main_drawer); + headerMainBinding.ownerAccounts.setImageResource(R.drawable.ic_baseline_arrow_drop_down_24); + headerMenuOpen = false; + } + }); + headerMainBinding.headerOptionInfo.setOnClickListener(v -> { + PopupMenu popup = new PopupMenu(new ContextThemeWrapper(BaseMainActivity.this, Helper.popupStyle()), headerMainBinding.headerOptionInfo); + popup.getMenuInflater() + .inflate(R.menu.main, popup.getMenu()); + + popup.setOnMenuItemClickListener(item -> { + int itemId = item.getItemId(); + if (itemId == R.id.action_logout_account) { + AlertDialog.Builder alt_bld = new AlertDialog.Builder(BaseMainActivity.this, Helper.dialogStyle()); + alt_bld.setTitle(R.string.action_logout); + alt_bld.setMessage(getString(R.string.logout_account_confirmation, account.mastodon_account.username, account.instance)); + alt_bld.setPositiveButton(R.string.action_logout, (dialog, id) -> { + dialog.dismiss(); + try { + Helper.removeAccount(BaseMainActivity.this, null); + } catch (DBException e) { + e.printStackTrace(); + } + }); + alt_bld.setNegativeButton(R.string.cancel, (dialog, id) -> dialog.dismiss()); + AlertDialog alert = alt_bld.create(); + alert.show(); + return true; + } else if (itemId == R.id.action_about_instance) { + Intent intent = new Intent(BaseMainActivity.this, InstanceActivity.class); + startActivity(intent); + return true; + } else if (itemId == R.id.action_cache) { + new Helper.CacheTask(BaseMainActivity.this); + return true; + } else if (itemId == R.id.action_proxy) { + Intent intent = new Intent(BaseMainActivity.this, ProxyActivity.class); + startActivity(intent); + return true; + } + return true; + }); + popup.show(); + }); + account = null; + //Update account details + new Thread(() -> { + try { + account = new Account(BaseMainActivity.this).getConnectedAccount(); + } catch (DBException e) { + e.printStackTrace(); + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + Runnable myRunnable = () -> { + if (account == null) { + //It is not, the user is redirected to the login page + Intent myIntent = new Intent(BaseMainActivity.this, LoginActivity.class); + startActivity(myIntent); + finish(); + return; + } + currentInstance = account.instance; + currentUserID = account.user_id; + accountWeakReference = new WeakReference<>(account); + binding.profilePicture.setOnClickListener(v -> binding.drawerLayout.openDrawer(GravityCompat.START)); + Helper.loadPP(binding.profilePicture, account); + headerMainBinding.accountAcc.setText(String.format("%s@%s", account.mastodon_account.username, account.instance)); + if (account.mastodon_account.display_name.isEmpty()) { + account.mastodon_account.display_name = account.mastodon_account.acct; + } + headerMainBinding.accountName.setText(account.mastodon_account.display_name); + + Helper.loadPP(headerMainBinding.accountProfilePicture, account); + + /* + * Some general data are loaded when the app starts such; + * - Instance info (for limits) + * - Emoji for picker + * - Filters for timelines + * - Pinned timelines (in app feature) + */ + + //Update emoji in db for the current instance + new ViewModelProvider(BaseMainActivity.this).get(InstancesVM.class).getEmoji(currentInstance); + //Retrieve instance info + new ViewModelProvider(BaseMainActivity.this).get(InstancesVM.class).getInstance(currentInstance) + .observe(BaseMainActivity.this, instance -> instanceInfo = instance.info); + //Retrieve filters + new ViewModelProvider(BaseMainActivity.this).get(AccountsVM.class).getFilters(currentInstance, currentToken) + .observe(BaseMainActivity.this, filters -> mainFilters = filters); + new ViewModelProvider(BaseMainActivity.this).get(AccountsVM.class).getConnectedAccount(currentInstance, currentToken) + .observe(BaseMainActivity.this, account1 -> { + BaseMainActivity.accountWeakReference.get().mastodon_account = account1; + }); + //Update pinned timelines + new ViewModelProvider(BaseMainActivity.this).get(TopBarVM.class).getDBPinned() + .observe(this, pinned -> { + this.pinned = pinned; + //First it's taken from db (last stored values) + PinnedTimelineHelper.redrawTopBarPinned(BaseMainActivity.this, binding, pinned, null); + //Fetch remote lists for the authenticated account and update them + new ViewModelProvider(BaseMainActivity.this).get(TimelinesVM.class).getLists(currentInstance, currentToken) + .observe(this, mastodonLists -> PinnedTimelineHelper.redrawTopBarPinned(BaseMainActivity.this, binding, pinned, mastodonLists)); + }); + }; + mainHandler.post(myRunnable); + }).start(); + //Toolbar search + binding.toolbarSearch.setOnQueryTextListener(new SearchView.OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String query) { + //Hide keyboard + InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(binding.toolbarSearch.getWindowToken(), 0); + query = query.replaceAll("^#+", ""); + Intent intent; + intent = new Intent(BaseMainActivity.this, SearchResultTabActivity.class); + intent.putExtra(Helper.ARG_SEARCH_KEYWORD, query); + startActivity(intent); + binding.toolbarSearch.setQuery("", false); + binding.toolbarSearch.setIconified(true); + return false; + } + + @Override + public boolean onQueryTextChange(String newText) { + return false; + } + }); + binding.toolbarSearch.setOnCloseListener(() -> { + binding.tabLayout.setVisibility(View.VISIBLE); + return false; + }); + + PushHelper.startStreaming(BaseMainActivity.this); + + binding.toolbarSearch.setOnSearchClickListener(v -> binding.tabLayout.setVisibility(View.VISIBLE)); + //For receiving data from other activities + LocalBroadcastManager.getInstance(BaseMainActivity.this).registerReceiver(broadcast_data, new IntentFilter(Helper.BROADCAST_DATA)); + } + + public void refreshFragment() { + if (binding.viewPager.getAdapter() != null) { + binding.viewPager.getAdapter().notifyDataSetChanged(); + } + } + + @Override + protected void onDestroy() { + LocalBroadcastManager.getInstance(BaseMainActivity.this).unregisterReceiver(broadcast_data); + if (networkStateReceiver != null) { + try { + unregisterReceiver(networkStateReceiver); + } catch (IllegalArgumentException illegalArgumentException) { + illegalArgumentException.printStackTrace(); + } + } + SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(BaseMainActivity.this); + boolean clearCacheExit = sharedpreferences.getBoolean(getString(R.string.SET_CLEAR_CACHE_EXIT), false); + //Clear cache when leaving - Default = false + if (clearCacheExit) { + new Thread(() -> { + try { + if (getCacheDir().getParentFile() != null) { + String path = getCacheDir().getParentFile().getPath(); + File dir = new File(path); + if (dir.isDirectory()) { + deleteDir(dir); + } + } + } catch (Exception ignored) { + } + }).start(); + } + super.onDestroy(); + } + + @Override + protected void onResume() { + super.onResume(); + } + + public void redrawPinned(List mastodonLists) { + int currentItem = binding.viewPager.getCurrentItem(); + new ViewModelProvider(BaseMainActivity.this).get(TopBarVM.class).getDBPinned() + .observe(this, pinned -> { + this.pinned = pinned; + //First it's taken from db (last stored values) + PinnedTimelineHelper.redrawTopBarPinned(BaseMainActivity.this, binding, pinned, mastodonLists); + binding.viewPager.setCurrentItem(currentItem); + }); + } + + @Override + public void onBackPressed() { + if (binding.drawerLayout.isDrawerOpen(GravityCompat.START)) { + binding.drawerLayout.closeDrawer(GravityCompat.START); + } else { + int fragments = getSupportFragmentManager().getBackStackEntryCount(); + if (fragments == 1) { + finish(); + } else if (getSupportFragmentManager().getBackStackEntryCount() > 1) { + getSupportFragmentManager().popBackStack(); + List fragmentList = getSupportFragmentManager().getFragments(); + for (Fragment fragment : fragmentList) { + if (fragment != null && fragment.isVisible()) { + + if (fragment instanceof FragmentMastodonTimeline) { + currentFragment = fragment; + getSupportFragmentManager().beginTransaction().show(currentFragment).commit(); + } + + } + } + } else { + super.onBackPressed(); + } + } + } + + public boolean getFloatingVisibility() { + return binding.compose.getVisibility() == View.VISIBLE; + } + + public void manageFloatingButton(boolean display) { + if (display) { + binding.compose.show(); + } else { + binding.compose.hide(); + } + } + + /* @Override + public boolean onCreateOptionsMenu(@NonNull Menu menu) { + // Inflate the menu; this adds items to the action bar if it is present. + getMenuInflater().inflate(R.menu.main, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == R.id.action_logout) { + AlertDialog.Builder alt_bld = new AlertDialog.Builder(BaseMainActivity.this, Helper.dialogStyle()); + alt_bld.setTitle(R.string.action_logout); + alt_bld.setMessage(getString(R.string.logout_account_confirmation, account.mastodon_account.username, account.instance)); + alt_bld.setPositiveButton(R.string.action_logout, (dialog, id) -> { + dialog.dismiss(); + try { + Helper.removeAccount(BaseMainActivity.this, null); + } catch (DBException e) { + e.printStackTrace(); + } + }); + alt_bld.setNegativeButton(R.string.cancel, (dialog, id) -> dialog.dismiss()); + AlertDialog alert = alt_bld.create(); + alert.show(); + + } + return true; + }*/ + + @Override + public boolean onSupportNavigateUp() { + NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment_content_main); + //unselect all tag elements + for (PinnedTimeline pinnedTimeline : pinned.pinnedTimelines) { + pinnedTimeline.isSelected = false; + } + return NavigationUI.navigateUp(navController, mAppBarConfiguration) + || super.onSupportNavigateUp(); + } + + @Override + public void networkAvailable() { + networkAvailable = status.CONNECTED; + } + + @Override + public void networkUnavailable() { + networkAvailable = DISCONNECTED; + } + + + public enum status { + UNKNOWN, + CONNECTED, + DISCONNECTED + } +} \ No newline at end of file diff --git a/app/src/main/java/app/fedilab/android/InstancesSocialService.java b/app/src/main/java/app/fedilab/android/InstancesSocialService.java new file mode 100644 index 000000000..580bf89ae --- /dev/null +++ b/app/src/main/java/app/fedilab/android/InstancesSocialService.java @@ -0,0 +1,29 @@ +package app.fedilab.android; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + + +import app.fedilab.android.client.entities.InstanceSocial; +import retrofit2.Call; +import retrofit2.http.GET; +import retrofit2.http.Header; +import retrofit2.http.Query; + +public interface InstancesSocialService { + + @GET("instances/search?name=true") + Call getInstances(@Header("Authorization") String token, @Query("q") String search); + +} diff --git a/app/src/main/java/app/fedilab/android/MainApplication.java b/app/src/main/java/app/fedilab/android/MainApplication.java new file mode 100644 index 000000000..334b693d8 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/MainApplication.java @@ -0,0 +1,90 @@ +package app.fedilab.android; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + + +import android.content.Context; +import android.content.SharedPreferences; +import android.os.StrictMode; + +import androidx.multidex.MultiDex; +import androidx.multidex.MultiDexApplication; +import androidx.preference.PreferenceManager; + +import com.jaredrummler.cyanea.Cyanea; +import com.jaredrummler.cyanea.prefs.CyaneaTheme; + +import org.acra.ACRA; +import org.acra.annotation.AcraNotification; +import org.acra.config.CoreConfigurationBuilder; +import org.acra.config.LimiterConfigurationBuilder; +import org.acra.config.MailSenderConfigurationBuilder; +import org.acra.data.StringFormat; + +import java.util.List; + +import es.dmoral.toasty.Toasty; + + +@AcraNotification( + resIcon = R.mipmap.ic_launcher, resTitle = R.string.crash_title, resChannelName = R.string.set_crash_reports, resText = R.string.crash_message) + +public class MainApplication extends MultiDexApplication { + + + private static MainApplication app; + + public static MainApplication getApp() { + return app; + } + + @Override + public void onCreate() { + super.onCreate(); + app = this; + SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(MainApplication.this); + + + Cyanea.init(this, super.getResources()); + List list = CyaneaTheme.Companion.from(getAssets(), "themes/cyanea_themes.json"); + int theme = sharedpreferences.getInt(getString(R.string.SET_THEME), 0); + boolean custom_theme = sharedpreferences.getBoolean("use_custom_theme", false); + if (!custom_theme) { + list.get(theme).apply(Cyanea.getInstance()); + } + StrictMode.VmPolicy.Builder builder = new StrictMode.VmPolicy.Builder(); + StrictMode.setVmPolicy(builder.build()); + + + boolean send_crash_reports = sharedpreferences.getBoolean(getString(R.string.SET_SEND_CRASH_REPORTS), false); + if (send_crash_reports) { + CoreConfigurationBuilder ACRABuilder = new CoreConfigurationBuilder(this); + ACRABuilder.setBuildConfigClass(BuildConfig.class).setReportFormat(StringFormat.KEY_VALUE_LIST); + int versionCode = BuildConfig.VERSION_CODE; + ACRABuilder.getPluginConfigurationBuilder(MailSenderConfigurationBuilder.class).setReportAsFile(true).setMailTo("hello@fedilab.app").setSubject("[Fedilab] - Crash Report " + versionCode).setEnabled(true); + ACRABuilder.getPluginConfigurationBuilder(LimiterConfigurationBuilder.class).setEnabled(true); + ACRA.init(this, ACRABuilder); + } + + Toasty.Config.getInstance().apply(); + } + + + @Override + protected void attachBaseContext(Context base) { + super.attachBaseContext(base); + MultiDex.install(MainApplication.this); + } +} diff --git a/app/src/main/java/app/fedilab/android/activities/ActionActivity.java b/app/src/main/java/app/fedilab/android/activities/ActionActivity.java new file mode 100644 index 000000000..2320778b8 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/activities/ActionActivity.java @@ -0,0 +1,129 @@ +package app.fedilab.android.activities; +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import android.os.Bundle; +import android.view.MenuItem; + +import androidx.annotation.Nullable; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentTransaction; + +import app.fedilab.android.R; +import app.fedilab.android.client.entities.Timeline; +import app.fedilab.android.databinding.ActivityActionsBinding; +import app.fedilab.android.helper.Helper; +import app.fedilab.android.helper.ThemeHelper; +import app.fedilab.android.ui.fragment.timeline.FragmentMastodonAccount; +import app.fedilab.android.ui.fragment.timeline.FragmentMastodonTimeline; + +public class ActionActivity extends BaseActivity { + + private ActivityActionsBinding binding; + private boolean canGoBack; + private FragmentMastodonTimeline fragmentMastodonTimeline; + private FragmentMastodonAccount fragmentMastodonAccount; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + ThemeHelper.applyThemeBar(this); + binding = ActivityActionsBinding.inflate(getLayoutInflater()); + setContentView(binding.getRoot()); + + if (getSupportActionBar() != null) + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + canGoBack = false; + binding.favourites.setOnClickListener(v -> displayTimeline(Timeline.TimeLineEnum.FAVOURITE_TIMELINE)); + binding.bookmarks.setOnClickListener(v -> displayTimeline(Timeline.TimeLineEnum.BOOKMARK_TIMELINE)); + binding.muted.setOnClickListener(v -> displayTimeline(Timeline.TimeLineEnum.MUTED_TIMELINE)); + binding.blocked.setOnClickListener(v -> displayTimeline(Timeline.TimeLineEnum.BLOCKED_TIMELINE)); + } + + private void displayTimeline(Timeline.TimeLineEnum type) { + canGoBack = true; + if (type == Timeline.TimeLineEnum.MUTED_TIMELINE || type == Timeline.TimeLineEnum.BLOCKED_TIMELINE) { + + ThemeHelper.slideViewsToLeft(binding.buttonContainer, binding.fragmentContainer, () -> { + fragmentMastodonAccount = new FragmentMastodonAccount(); + Bundle bundle = new Bundle(); + bundle.putSerializable(Helper.ARG_TIMELINE_TYPE, type); + fragmentMastodonAccount.setArguments(bundle); + FragmentManager fragmentManager = getSupportFragmentManager(); + FragmentTransaction fragmentTransaction = + fragmentManager.beginTransaction(); + fragmentTransaction.replace(R.id.fragment_container, fragmentMastodonAccount); + fragmentTransaction.commit(); + }); + + } else { + + ThemeHelper.slideViewsToLeft(binding.buttonContainer, binding.fragmentContainer, () -> { + fragmentMastodonTimeline = new FragmentMastodonTimeline(); + Bundle bundle = new Bundle(); + bundle.putSerializable(Helper.ARG_TIMELINE_TYPE, type); + fragmentMastodonTimeline.setArguments(bundle); + FragmentManager fragmentManager = getSupportFragmentManager(); + FragmentTransaction fragmentTransaction = + fragmentManager.beginTransaction(); + fragmentTransaction.replace(R.id.fragment_container, fragmentMastodonTimeline); + fragmentTransaction.commit(); + }); + + } + switch (type) { + case MUTED_TIMELINE: + setTitle(R.string.muted_menu); + break; + case FAVOURITE_TIMELINE: + setTitle(R.string.favourite); + break; + case BLOCKED_TIMELINE: + setTitle(R.string.blocked_menu); + break; + case BOOKMARK_TIMELINE: + setTitle(R.string.bookmarks); + break; + } + } + + @Override + public void onBackPressed() { + if (canGoBack) { + canGoBack = false; + ThemeHelper.slideViewsToRight(binding.fragmentContainer, binding.buttonContainer, () -> { + if (fragmentMastodonTimeline != null) { + fragmentMastodonTimeline.onDestroyView(); + } + if (fragmentMastodonAccount != null) { + fragmentMastodonAccount.onDestroyView(); + } + }); + setTitle(R.string.interactions); + } else { + super.onBackPressed(); + } + + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == android.R.id.home) { + onBackPressed(); + return true; + } + return super.onOptionsItemSelected(item); + } +} diff --git a/app/src/main/java/app/fedilab/android/activities/BaseActivity.java b/app/src/main/java/app/fedilab/android/activities/BaseActivity.java new file mode 100644 index 000000000..f732a059f --- /dev/null +++ b/app/src/main/java/app/fedilab/android/activities/BaseActivity.java @@ -0,0 +1,45 @@ +package app.fedilab.android.activities; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + + +import android.annotation.SuppressLint; +import android.os.Bundle; + +import androidx.annotation.Nullable; + +import com.jaredrummler.cyanea.app.CyaneaAppCompatActivity; +import com.vanniktech.emoji.EmojiManager; +import com.vanniktech.emoji.one.EmojiOneProvider; + +import app.fedilab.android.helper.Helper; + + +@SuppressLint("Registered") +public class BaseActivity extends CyaneaAppCompatActivity { + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Helper.setLocale(this); + } + + + static { + Helper.installProvider(); + EmojiManager.install(new EmojiOneProvider()); + } + +} diff --git a/app/src/main/java/app/fedilab/android/activities/BaseFragmentActivity.java b/app/src/main/java/app/fedilab/android/activities/BaseFragmentActivity.java new file mode 100644 index 000000000..e5364165e --- /dev/null +++ b/app/src/main/java/app/fedilab/android/activities/BaseFragmentActivity.java @@ -0,0 +1,36 @@ +package app.fedilab.android.activities; +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import android.annotation.SuppressLint; + +import com.jaredrummler.cyanea.app.CyaneaFragmentActivity; +import com.vanniktech.emoji.EmojiManager; +import com.vanniktech.emoji.one.EmojiOneProvider; + +import app.fedilab.android.helper.Helper; + + +@SuppressLint("Registered") +public class BaseFragmentActivity extends CyaneaFragmentActivity { + + + static { + Helper.installProvider(); + EmojiManager.install(new EmojiOneProvider()); + } + + +} diff --git a/app/src/main/java/app/fedilab/android/activities/ComposeActivity.java b/app/src/main/java/app/fedilab/android/activities/ComposeActivity.java new file mode 100644 index 000000000..823bcdc7f --- /dev/null +++ b/app/src/main/java/app/fedilab/android/activities/ComposeActivity.java @@ -0,0 +1,599 @@ +package app.fedilab.android.activities; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + + +import static app.fedilab.android.ui.drawer.ComposeAdapter.prepareDraft; + +import android.Manifest; +import android.annotation.SuppressLint; +import android.content.ClipData; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.Menu; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.View; +import android.widget.LinearLayout; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AlertDialog; +import androidx.core.content.ContextCompat; +import androidx.lifecycle.ViewModelProvider; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.work.Data; +import androidx.work.OneTimeWorkRequest; +import androidx.work.WorkManager; + +import java.io.File; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.TimeUnit; + +import app.fedilab.android.BaseMainActivity; +import app.fedilab.android.R; +import app.fedilab.android.client.entities.Account; +import app.fedilab.android.client.entities.StatusDraft; +import app.fedilab.android.client.mastodon.entities.Context; +import app.fedilab.android.client.mastodon.entities.Mention; +import app.fedilab.android.client.mastodon.entities.ScheduledStatus; +import app.fedilab.android.client.mastodon.entities.Status; +import app.fedilab.android.databinding.ActivityPaginationBinding; +import app.fedilab.android.databinding.PopupContactBinding; +import app.fedilab.android.exception.DBException; +import app.fedilab.android.helper.DividerDecorationSimple; +import app.fedilab.android.helper.Helper; +import app.fedilab.android.helper.MastodonHelper; +import app.fedilab.android.helper.MediaHelper; +import app.fedilab.android.helper.SpannableHelper; +import app.fedilab.android.helper.ThemeHelper; +import app.fedilab.android.jobs.ScheduleThreadWorker; +import app.fedilab.android.services.PostMessageService; +import app.fedilab.android.ui.drawer.AccountsReplyAdapter; +import app.fedilab.android.ui.drawer.ComposeAdapter; +import app.fedilab.android.viewmodel.mastodon.AccountsVM; +import app.fedilab.android.viewmodel.mastodon.StatusesVM; +import es.dmoral.toasty.Toasty; + +public class ComposeActivity extends BaseActivity implements ComposeAdapter.ManageDrafts, AccountsReplyAdapter.ActionDone { + + + public static final int MY_PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE = 754; + public static final int REQUEST_AUDIO_PERMISSION_RESULT = 1653; + public static final int PICK_MEDIA = 5700; + public static final int TAKE_PHOTO = 5600; + + private List statusList; + private Status statusReply, statusMention; + private StatusDraft statusDraft; + private ComposeAdapter composeAdapter; + private ActivityPaginationBinding binding; + private Account account; + private String instance, token; + private Uri photoFileUri; + private ScheduledStatus scheduledStatus; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + ThemeHelper.applyTheme(this); + binding = ActivityPaginationBinding.inflate(getLayoutInflater()); + setContentView(binding.getRoot()); + setSupportActionBar(binding.toolbar); + ActionBar actionBar = getSupportActionBar(); + //Remove title + if (actionBar != null) { + actionBar.setDisplayShowTitleEnabled(false); + } + if (getSupportActionBar() != null) { + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + getSupportActionBar().setDisplayShowHomeEnabled(true); + } + statusList = new ArrayList<>(); + Bundle b = getIntent().getExtras(); + if (b != null) { + statusReply = (Status) b.getSerializable(Helper.ARG_STATUS_REPLY); + statusDraft = (StatusDraft) b.getSerializable(Helper.ARG_STATUS_DRAFT); + scheduledStatus = (ScheduledStatus) b.getSerializable(Helper.ARG_STATUS_SCHEDULED); + statusMention = (Status) b.getSerializable(Helper.ARG_STATUS_MENTION); + account = (Account) b.getSerializable(Helper.ARG_ACCOUNT); + instance = b.getString(Helper.ARG_INSTANCE, BaseMainActivity.currentInstance); + token = b.getString(Helper.ARG_TOKEN, BaseMainActivity.currentToken); + } + binding.toolbar.setPopupTheme(Helper.popupStyle()); + //Edit a scheduled status from server + if (scheduledStatus != null) { + statusDraft = new StatusDraft(); + List statuses = new ArrayList<>(); + Status status = new Status(); + status.text = scheduledStatus.params.text; + status.in_reply_to_id = scheduledStatus.params.in_reply_to_id; + status.poll = scheduledStatus.params.poll; + + if (scheduledStatus.params.media_ids != null && scheduledStatus.params.media_ids.size() > 0) { + status.media_attachments = new ArrayList<>(); + new Thread(() -> { + StatusesVM statusesVM = new ViewModelProvider(ComposeActivity.this).get(StatusesVM.class); + for (String attachmentId : scheduledStatus.params.media_ids) { + statusesVM.getAttachment(instance, token, attachmentId) + .observe(ComposeActivity.this, attachment -> status.media_attachments.add(attachment)); + } + }).start(); + } + status.sensitive = scheduledStatus.params.sensitive; + status.spoiler_text = scheduledStatus.params.spoiler_text; + status.visibility = scheduledStatus.params.visibility; + statusDraft.statusDraftList = statuses; + } + if (instance == null) { + instance = BaseMainActivity.currentInstance; + } + if (token == null) { + token = BaseMainActivity.currentToken; + } + if (account == null) { + account = BaseMainActivity.accountWeakReference.get(); + } + StatusesVM statusesVM = new ViewModelProvider(ComposeActivity.this).get(StatusesVM.class); + //Empty compose + List statusDraftList = new ArrayList<>(); + Status status = new Status(); + statusDraftList.add(status); + + //Restore a draft with all messages + if (statusDraft != null && statusDraft.statusReplyList != null) { + new Thread(() -> { + statusDraft.statusReplyList = SpannableHelper.convertStatus(getApplication().getApplicationContext(), statusDraft.statusReplyList); + Handler mainHandler = new Handler(Looper.getMainLooper()); + Runnable myRunnable = () -> { + if (statusDraft.statusReplyList != null) { + statusList.addAll(statusDraft.statusReplyList); + binding.recyclerView.addItemDecoration(new DividerDecorationSimple(ComposeActivity.this, statusList)); + } + int statusCount = statusList.size(); + statusList.addAll(statusDraft.statusDraftList); + composeAdapter = new ComposeAdapter(statusList, statusCount, account); + composeAdapter.manageDrafts = this; + LinearLayoutManager mLayoutManager = new LinearLayoutManager(ComposeActivity.this); + binding.recyclerView.setLayoutManager(mLayoutManager); + binding.recyclerView.setAdapter(composeAdapter); + binding.recyclerView.scrollToPosition(composeAdapter.getItemCount() - 1); + }; + mainHandler.post(myRunnable); + }).start(); + + } else if (statusReply != null) { + new Thread(() -> { + statusReply = SpannableHelper.convertStatus(getApplication().getApplicationContext(), statusReply); + Handler mainHandler = new Handler(Looper.getMainLooper()); + Runnable myRunnable = () -> { + statusList.add(statusReply); + int statusCount = statusList.size(); + statusDraftList.get(0).in_reply_to_id = statusReply.id; + statusDraftList.get(0).mentions = statusReply.mentions; + if (statusDraftList.get(0).mentions == null) { + statusDraftList.get(0).mentions = new ArrayList<>(); + } + //We will add the mentioned account in mention if not the current user nor if it is already mentioned + if (statusReply.account != null && statusReply.account.acct != null && !statusReply.account.id.equals(BaseMainActivity.currentUserID)) { + boolean canBeAdded = true; + for (Mention mention : statusDraftList.get(0).mentions) { + if (mention.acct.compareToIgnoreCase(statusReply.account.acct) == 0) { + mention.id = null; + canBeAdded = false; + } + } + if (canBeAdded) { + Mention mention = new Mention(); + mention.acct = "@" + statusReply.account.acct; + mention.url = statusReply.account.url; + mention.username = statusReply.account.username; + statusDraftList.get(0).mentions.add(mention); + } + } + //StatusDraftList at this point should only have one element + statusList.addAll(statusDraftList); + composeAdapter = new ComposeAdapter(statusList, statusCount, account); + composeAdapter.manageDrafts = this; + LinearLayoutManager mLayoutManager = new LinearLayoutManager(ComposeActivity.this); + binding.recyclerView.setLayoutManager(mLayoutManager); + binding.recyclerView.setAdapter(composeAdapter); + statusesVM.getContext(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, statusReply.id) + .observe(ComposeActivity.this, this::initializeContextView); + }; + mainHandler.post(myRunnable); + }).start(); + } else { + //Compose without replying + statusList.addAll(statusDraftList); + composeAdapter = new ComposeAdapter(statusList, 0, account); + composeAdapter.manageDrafts = this; + LinearLayoutManager mLayoutManager = new LinearLayoutManager(ComposeActivity.this); + binding.recyclerView.setLayoutManager(mLayoutManager); + binding.recyclerView.setAdapter(composeAdapter); + if (statusMention != null) { + composeAdapter.loadMentions(statusMention); + } + + } + MastodonHelper.loadPPMastodon(binding.profilePicture, account.mastodon_account); + } + + @Override + public void onBackPressed() { + storeDraftWarning(); + } + + private void storeDraftWarning() { + if (statusDraft == null) { + statusDraft = prepareDraft(statusList, composeAdapter, account.instance, account.user_id); + } + if (canBeSent(statusDraft)) { + AlertDialog.Builder alt_bld = new AlertDialog.Builder(ComposeActivity.this, Helper.dialogStyle()); + alt_bld.setMessage(R.string.save_draft); + alt_bld.setPositiveButton(R.string.save, (dialog, id) -> { + dialog.dismiss(); + storeDraft(false); + finish(); + + }); + alt_bld.setNegativeButton(R.string.no, (dialog, id) -> { + dialog.dismiss(); + finish(); + }); + AlertDialog alert = alt_bld.create(); + alert.show(); + } else { + finish(); + } + } + + /** + * Intialize the common view for the context + * + * @param context {@link Context} + */ + private void initializeContextView(final Context context) { + + if (context == null) { + return; + } + //Build the array of statuses + statusList.addAll(0, context.ancestors); + composeAdapter.setStatusCount(context.ancestors.size() + 1); + composeAdapter.notifyItemRangeInserted(0, context.ancestors.size()); + if (binding.recyclerView.getItemDecorationCount() > 0) { + for (int i = 0; i < binding.recyclerView.getItemDecorationCount(); i++) { + binding.recyclerView.removeItemDecorationAt(i); + } + } + binding.recyclerView.addItemDecoration(new DividerDecorationSimple(ComposeActivity.this, statusList)); + binding.recyclerView.scrollToPosition(composeAdapter.getItemCount() - 1); + } + + + @Override + public boolean onCreateOptionsMenu(@NonNull Menu menu) { + // Inflate the menu; this adds items to the action bar if it is present. + getMenuInflater().inflate(R.menu.menu_compose, menu); + return true; + } + + + @SuppressLint("ClickableViewAccessibility") + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == android.R.id.home) { + storeDraftWarning(); + return true; + } else if (item.getItemId() == R.id.action_photo_camera) { + photoFileUri = MediaHelper.dispatchTakePictureIntent(ComposeActivity.this); + } else if (item.getItemId() == R.id.action_contacts) { + AlertDialog.Builder builderSingle = new AlertDialog.Builder(ComposeActivity.this, Helper.dialogStyle()); + + builderSingle.setTitle(getString(R.string.select_accounts)); + PopupContactBinding popupContactBinding = PopupContactBinding.inflate(getLayoutInflater(), new LinearLayout(ComposeActivity.this), false); + popupContactBinding.loader.setVisibility(View.VISIBLE); + AccountsVM accountsVM = new ViewModelProvider(ComposeActivity.this).get(AccountsVM.class); + accountsVM.searchAccounts(instance, token, "", 10, false, true) + .observe(ComposeActivity.this, accounts -> onRetrieveContact(popupContactBinding, accounts)); + + popupContactBinding.searchAccount.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + if (count > 0) { + popupContactBinding.searchAccount.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.ic_baseline_close_24, 0); + } else { + popupContactBinding.searchAccount.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.ic_baseline_search_24, 0); + } + } + + @Override + public void afterTextChanged(Editable s) { + if (s != null && s.length() > 0) { + accountsVM.searchAccounts(instance, token, s.toString().trim(), 10, false, true) + .observe(ComposeActivity.this, accounts -> onRetrieveContact(popupContactBinding, accounts)); + } + } + }); + popupContactBinding.searchAccount.setOnTouchListener((v, event) -> { + final int DRAWABLE_RIGHT = 2; + if (event.getAction() == MotionEvent.ACTION_UP) { + if (popupContactBinding.searchAccount.length() > 0 && event.getRawX() >= (popupContactBinding.searchAccount.getRight() - popupContactBinding.searchAccount.getCompoundDrawables()[DRAWABLE_RIGHT].getBounds().width())) { + popupContactBinding.searchAccount.setText(""); + accountsVM.searchAccounts(instance, token, "", 10, false, true) + .observe(ComposeActivity.this, accounts -> onRetrieveContact(popupContactBinding, accounts)); + } + } + + return false; + }); + builderSingle.setView(popupContactBinding.getRoot()); + builderSingle.setNegativeButton(R.string.validate, (dialog, which) -> { + dialog.dismiss(); + composeAdapter.putCursor(); + }); + builderSingle.show(); + } else if (item.getItemId() == R.id.action_microphone) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) == + PackageManager.PERMISSION_GRANTED) { + MediaHelper.recordAudio(ComposeActivity.this, file -> { + List uris = new ArrayList<>(); + uris.add(Uri.fromFile(new File(file))); + composeAdapter.addAttachment(-1, uris); + }); + } else { + if (shouldShowRequestPermissionRationale(Manifest.permission.RECORD_AUDIO)) { + Toast.makeText(this, + getString(R.string.audio), Toast.LENGTH_SHORT).show(); + } + requestPermissions(new String[]{Manifest.permission.RECORD_AUDIO + }, REQUEST_AUDIO_PERMISSION_RESULT); + } + + } else { + MediaHelper.recordAudio(ComposeActivity.this, file -> { + List uris = new ArrayList<>(); + uris.add(Uri.fromFile(new File(file))); + composeAdapter.addAttachment(-1, uris); + }); + } + } else if (item.getItemId() == R.id.action_schedule) { + if (statusDraft == null) { + statusDraft = prepareDraft(statusList, composeAdapter, account.instance, account.user_id); + } + if (canBeSent(statusDraft)) { + MediaHelper.scheduleMessage(ComposeActivity.this, date -> storeDraft(true, date)); + } else { + Toasty.info(ComposeActivity.this, getString(R.string.toot_error_no_content), Toasty.LENGTH_SHORT).show(); + } + } + return true; + } + + + private void onRetrieveContact(PopupContactBinding binding, List accounts) { + binding.loader.setVisibility(View.GONE); + if (accounts == null) { + accounts = new ArrayList<>(); + } + List checkedValues = new ArrayList<>(); + List contacts = new ArrayList<>(accounts); + for (app.fedilab.android.client.mastodon.entities.Account account : contacts) { + checkedValues.add(composeAdapter.getLastComposeContent().contains("@" + account.acct)); + } + AccountsReplyAdapter contactAdapter = new AccountsReplyAdapter(contacts, checkedValues); + binding.lvAccountsSearch.setAdapter(contactAdapter); + binding.lvAccountsSearch.setLayoutManager(new LinearLayoutManager(ComposeActivity.this)); + } + + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + List uris = new ArrayList<>(); + if (requestCode >= PICK_MEDIA && resultCode == RESULT_OK) { + ClipData clipData = data.getClipData(); + int position = requestCode - PICK_MEDIA; + if (clipData != null) { + for (int i = 0; i < clipData.getItemCount(); i++) { + ClipData.Item item = clipData.getItemAt(i); + uris.add(item.getUri()); + } + } else { + uris.add(data.getData()); + } + composeAdapter.addAttachment(position, uris); + } else if (requestCode == TAKE_PHOTO && resultCode == RESULT_OK) { + uris.add(photoFileUri); + composeAdapter.addAttachment(-1, uris); + } + } + + @Override + public void onItemDraftAdded(int position) { + Status status = new Status(); + status.mentions = statusList.get(position).mentions; + status.visibility = statusList.get(position).visibility; + status.spoiler_text = statusList.get(position).spoiler_text; + status.sensitive = statusList.get(position).sensitive; + statusList.add(status); + composeAdapter.notifyItemInserted(position + 1); + binding.recyclerView.smoothScrollToPosition(position + 1); + } + + @Override + public void onItemDraftDeleted(Status status, int position) { + statusList.remove(status); + composeAdapter.notifyItemRemoved(position); + } + + @Override + public void onSubmit(StatusDraft draft) { + //Store in drafts + if (statusDraft == null) { + statusDraft = draft; + } else { + statusDraft.statusDraftList = draft.statusDraftList; + } + storeDraft(true); + } + + + private void storeDraft(boolean sendMessage) { + storeDraft(sendMessage, null); + } + + private void storeDraft(boolean sendMessage, String scheduledDate) { + new Thread(() -> { + //Collect all statusCompose + List statusDrafts = new ArrayList<>(); + List statusReplies = new ArrayList<>(); + for (Status status : statusList) { + if (status.id == null) { + statusDrafts.add(status); + } else { + statusReplies.add(status); + } + } + if (statusDraft == null) { + statusDraft = new StatusDraft(ComposeActivity.this); + } else { + //Draft previously and date is changed + if (statusDraft.scheduled_at != null && scheduledDate != null && statusDraft.workerUuid != null) { + WorkManager.getInstance(ComposeActivity.this).cancelWorkById(statusDraft.workerUuid); + } + } + statusDraft.statusReplyList = statusReplies; + statusDraft.statusDraftList = statusDrafts; + statusDraft.instance = account.instance; + statusDraft.user_id = account.user_id; + if (!canBeSent(statusDraft)) { + return; + } + if (statusDraft.id > 0) { + try { + new StatusDraft(ComposeActivity.this).updateStatusDraft(statusDraft); + } catch (DBException e) { + e.printStackTrace(); + } + } else { + try { + statusDraft.id = new StatusDraft(ComposeActivity.this).insertStatusDraft(statusDraft); + } catch (DBException e) { + e.printStackTrace(); + } + } + //Only one single message scheduled + if (sendMessage && scheduledDate != null && statusDraft.statusDraftList.size() > 1) { + //Schedule a thread + SimpleDateFormat sdf = new SimpleDateFormat(Helper.SCHEDULE_DATE_FORMAT, Locale.getDefault()); + Date date; + try { + date = sdf.parse(scheduledDate); + long delayToPass = 0; + if (date != null) { + delayToPass = (date.getTime() - new Date().getTime()); + } + Data inputData = new Data.Builder() + .putString(Helper.ARG_INSTANCE, BaseMainActivity.currentInstance) + .putString(Helper.ARG_TOKEN, BaseMainActivity.currentToken) + .putString(Helper.ARG_USER_ID, BaseMainActivity.currentUserID) + .putLong(Helper.ARG_STATUS_DRAFT_ID, statusDraft.id) + .build(); + + OneTimeWorkRequest oneTimeWorkRequest = new OneTimeWorkRequest.Builder(ScheduleThreadWorker.class) + .setInputData(inputData) + .addTag(Helper.WORKER_SCHEDULED_STATUSES) + .setInitialDelay(delayToPass, TimeUnit.MILLISECONDS) + .build(); + statusDraft.workerUuid = oneTimeWorkRequest.getId(); + statusDraft.scheduled_at = date; + Handler mainHandler = new Handler(Looper.getMainLooper()); + Runnable myRunnable = () -> { + Toasty.info(ComposeActivity.this, getString(R.string.toot_scheduled), Toasty.LENGTH_LONG).show(); + finish(); + }; + mainHandler.post(myRunnable); + } catch (ParseException e) { + e.printStackTrace(); + } + + } else if (sendMessage) { + Intent intent = new Intent(ComposeActivity.this, PostMessageService.class); + intent.putExtra(Helper.ARG_STATUS_DRAFT, statusDraft); + intent.putExtra(Helper.ARG_INSTANCE, instance); + intent.putExtra(Helper.ARG_TOKEN, token); + intent.putExtra(Helper.ARG_SCHEDULED_DATE, scheduledDate); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + startForegroundService(intent); + } else { + startService(intent); + } + finish(); + } + + }).start(); + } + + + private boolean canBeSent(StatusDraft statusDraft) { + if (statusDraft == null || statusDraft.statusDraftList == null || statusDraft.statusDraftList.size() == 0) { + return false; + } + Status statusCheck = statusDraft.statusDraftList.get(0); + if (statusCheck == null) { + return false; + } + return (statusCheck.text != null && statusCheck.text.trim().length() != 0) + || (statusCheck.media_attachments != null && statusCheck.media_attachments.size() != 0) + || statusCheck.poll != null + || (statusCheck.spoiler_text != null && statusCheck.spoiler_text.trim().length() != 0); + } + + + @Override + public void onContactClick(boolean isChecked, String acct) { + composeAdapter.updateContent(isChecked, acct); + } + + + public enum mediaType { + PHOTO, + VIDEO, + AUDIO, + ALL + } + + +} \ No newline at end of file diff --git a/app/src/main/java/app/fedilab/android/activities/ContextActivity.java b/app/src/main/java/app/fedilab/android/activities/ContextActivity.java new file mode 100644 index 000000000..3b0e51309 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/activities/ContextActivity.java @@ -0,0 +1,143 @@ +package app.fedilab.android.activities; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + + +import android.content.SharedPreferences; +import android.content.res.Resources; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.view.Menu; +import android.view.MenuItem; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.ActionBar; +import androidx.fragment.app.Fragment; +import androidx.preference.PreferenceManager; + +import app.fedilab.android.BaseMainActivity; +import app.fedilab.android.R; +import app.fedilab.android.client.mastodon.entities.Status; +import app.fedilab.android.databinding.ActivityConversationBinding; +import app.fedilab.android.helper.Helper; +import app.fedilab.android.helper.MastodonHelper; +import app.fedilab.android.helper.SpannableHelper; +import app.fedilab.android.helper.ThemeHelper; +import app.fedilab.android.ui.fragment.timeline.FragmentMastodonContext; + +public class ContextActivity extends BaseActivity { + + public static boolean expand; + public static boolean displayCW; + Fragment currentFragment; + private Status focusedStatus; + private ActivityConversationBinding binding; + public static Resources.Theme theme; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + ThemeHelper.applyTheme(this); + binding = ActivityConversationBinding.inflate(getLayoutInflater()); + setContentView(binding.getRoot()); + setSupportActionBar(binding.toolbar); + ActionBar actionBar = getSupportActionBar(); + //Remove title + if (actionBar != null) { + actionBar.setDisplayShowTitleEnabled(false); + } + binding.title.setText(R.string.context_conversation); + if (getSupportActionBar() != null) { + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + getSupportActionBar().setDisplayShowHomeEnabled(true); + } + binding.toolbar.setPopupTheme(Helper.popupStyle()); + Bundle b = getIntent().getExtras(); + final SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(ContextActivity.this); + displayCW = sharedpreferences.getBoolean(getString(R.string.SET_EXPAND_CW), false); + focusedStatus = null; // or other values + if (b != null) + focusedStatus = (Status) b.getSerializable(Helper.ARG_STATUS); + if (focusedStatus == null) { + finish(); + return; + } + MastodonHelper.loadPPMastodon(binding.profilePicture, BaseMainActivity.accountWeakReference.get().mastodon_account); + Bundle bundle = new Bundle(); + new Thread(() -> { + focusedStatus = SpannableHelper.convertStatus(getApplication().getApplicationContext(), focusedStatus); + Handler mainHandler = new Handler(Looper.getMainLooper()); + Runnable myRunnable = () -> { + bundle.putSerializable(Helper.ARG_STATUS, focusedStatus); + currentFragment = Helper.addFragment(getSupportFragmentManager(), R.id.nav_host_fragment_content_main, new FragmentMastodonContext(), bundle, null, null); + }; + mainHandler.post(myRunnable); + }).start(); + } + + + @Override + public boolean onCreateOptionsMenu(@NonNull Menu menu) { + // Inflate the menu; this adds items to the action bar if it is present. + getMenuInflater().inflate(R.menu.menu_context, menu); + MenuItem itemExpand = menu.findItem(R.id.action_expand); + if (expand) { + itemExpand.setIcon(R.drawable.ic_baseline_expand_less_24); + } else { + itemExpand.setIcon(R.drawable.ic_baseline_expand_more_24); + } + MenuItem itemDisplayCW = menu.findItem(R.id.action_show_cw); + if (displayCW) { + itemDisplayCW.setIcon(R.drawable.ic_baseline_remove_red_eye_24); + } else { + itemDisplayCW.setIcon(R.drawable.ic_outline_remove_red_eye_24); + } + return true; + } + + public void setCurrentFragment(FragmentMastodonContext fragmentMastodonContext) { + currentFragment = fragmentMastodonContext; + } + + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == android.R.id.home) { + finish(); + return true; + } else if (item.getItemId() == R.id.action_expand) { + expand = !expand; + if (currentFragment != null && currentFragment instanceof FragmentMastodonContext) { + ((FragmentMastodonContext) currentFragment).redraw(); + } + invalidateOptionsMenu(); + } else if (item.getItemId() == R.id.action_show_cw) { + displayCW = !displayCW; + if (currentFragment != null && currentFragment instanceof FragmentMastodonContext) { + ((FragmentMastodonContext) currentFragment).refresh(); + } + invalidateOptionsMenu(); + } + return true; + } + + @Override + protected void onDestroy() { + super.onDestroy(); + binding = null; + currentFragment = null; + } +} \ No newline at end of file diff --git a/app/src/main/java/app/fedilab/android/activities/CustomSharingActivity.java b/app/src/main/java/app/fedilab/android/activities/CustomSharingActivity.java new file mode 100644 index 000000000..e16231f90 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/activities/CustomSharingActivity.java @@ -0,0 +1,250 @@ +package app.fedilab.android.activities; +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import android.content.SharedPreferences; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.text.Html; +import android.text.TextUtils; +import android.view.MenuItem; +import android.widget.Toast; + +import androidx.preference.PreferenceManager; + +import java.util.List; +import java.util.Set; + +import app.fedilab.android.BaseMainActivity; +import app.fedilab.android.R; +import app.fedilab.android.client.mastodon.entities.Attachment; +import app.fedilab.android.client.mastodon.entities.Emoji; +import app.fedilab.android.client.mastodon.entities.Status; +import app.fedilab.android.client.mastodon.entities.Tag; +import app.fedilab.android.databinding.ActivityCustomSharingBinding; +import app.fedilab.android.helper.Helper; +import app.fedilab.android.helper.ThemeHelper; +import app.fedilab.android.helper.customsharing.CustomSharingAsyncTask; +import app.fedilab.android.helper.customsharing.CustomSharingResponse; +import app.fedilab.android.helper.customsharing.OnCustomSharingInterface; +import es.dmoral.toasty.Toasty; + + +/** + * Created by Curtis on 13/02/2019. + * Share status metadata to remote content aggregators + */ + +public class CustomSharingActivity extends BaseActivity implements OnCustomSharingInterface { + + private String title, keywords, custom_sharing_url, encodedCustomSharingURL; + private String bundle_url; + private String bundle_source; + private String bundle_id; + private String bundle_content; + private String bundle_thumbnailurl; + private String bundle_creator; + private ActivityCustomSharingBinding binding; + private Status status; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + ThemeHelper.applyThemeBar(this); + SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(CustomSharingActivity.this); + binding = ActivityCustomSharingBinding.inflate(getLayoutInflater()); + setContentView(binding.getRoot()); + if (getSupportActionBar() != null) { + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + getSupportActionBar().setDisplayShowHomeEnabled(true); + } + Bundle b = getIntent().getExtras(); + status = null; + if (b != null) { + status = (Status) b.getSerializable(Helper.ARG_STATUS); + } + if (status == null) { + finish(); + return; + } + + bundle_creator = status.account.acct; + bundle_url = status.url; + bundle_id = status.uri; + bundle_source = status.account.url; + String bundle_tags = getTagsString(); + bundle_content = formatedContent(status.content, status.emojis); + if (status.card != null && status.card.image != null) { + bundle_thumbnailurl = status.card.image; + } else if (status.media_attachments != null && status.media_attachments.size() > 0) { + List mediaAttachments = status.media_attachments; + Attachment firstAttachment = mediaAttachments.get(0); + bundle_thumbnailurl = firstAttachment.preview_url; + } else { + bundle_thumbnailurl = status.account.avatar; + } + if (!bundle_creator.contains("@")) { + bundle_creator = bundle_creator + "@" + BaseMainActivity.accountWeakReference.get().instance; + } + + binding.setCustomSharingTitle.setEllipsize(TextUtils.TruncateAt.END); + //set text on title, description, and keywords + String[] lines = bundle_content.split("\n"); + //Remove tags in title + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + lines[0] = Html.fromHtml(lines[0], Html.FROM_HTML_MODE_LEGACY).toString(); + else + lines[0] = Html.fromHtml(lines[0]).toString(); + String newTitle; + if (lines[0].length() > 60) { + newTitle = lines[0].substring(0, 60) + '…'; + } else { + newTitle = lines[0]; + } + binding.setCustomSharingTitle.setText(newTitle); + String newDescription; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + newDescription = Html.fromHtml(bundle_content, Html.FROM_HTML_MODE_LEGACY).toString(); + else + newDescription = Html.fromHtml(bundle_content).toString(); + + binding.setCustomSharingDescription.setText(newDescription); + binding.setCustomSharingKeywords.setText(bundle_tags); + binding.setCustomSharingSave.setOnClickListener(v -> { + // obtain title, description, keywords + title = binding.setCustomSharingTitle.getText().toString(); + keywords = binding.setCustomSharingKeywords.getText().toString(); + CharSequence comma_only = ","; + CharSequence space_only = " "; + CharSequence double_space = " "; + keywords = keywords.replace(comma_only, space_only); + keywords = keywords.replace(double_space, space_only); + // Create encodedCustomSharingURL + custom_sharing_url = sharedpreferences.getString(getString(R.string.SET_CUSTOM_SHARING_URL), + "http://example.net/add?token=YOUR_TOKEN&url=${url}&title=${title}" + + "&source=${source}&id=${id}&description=${description}&keywords=${keywords}&creator=${creator}&thumbnailurl=${thumbnailurl}"); + encodedCustomSharingURL = encodeCustomSharingURL(); + new CustomSharingAsyncTask(CustomSharingActivity.this, encodedCustomSharingURL, CustomSharingActivity.this); + }); + } + + private String getTagsString() { + //iterate through tags and create comma delimited string of tag names + StringBuilder tag_names = new StringBuilder(); + for (Tag t : status.tags) { + if (tag_names.toString().equals("")) { + tag_names = new StringBuilder(t.name); + } else { + tag_names.append(", ").append(t.name); + } + } + return tag_names.toString(); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == android.R.id.home) { + finish(); + return true; + } + return super.onOptionsItemSelected(item); + } + + @Override + public void onCustomSharing(CustomSharingResponse customSharingResponse) { + binding.setCustomSharingSave.setEnabled(true); + if (customSharingResponse.getError() != null) { + Toasty.error(CustomSharingActivity.this, getString(R.string.toast_error), Toast.LENGTH_LONG).show(); + return; + } + String response = customSharingResponse.getResponse(); + Toasty.success(CustomSharingActivity.this, response, Toast.LENGTH_LONG).show(); + finish(); + } + + public String encodeCustomSharingURL() { + Uri uri = Uri.parse(custom_sharing_url); + String protocol = uri.getScheme(); + String server = uri.getAuthority(); + String path = uri.getPath(); + if (path != null) { + path = path.replaceAll("/", ""); + } + Uri.Builder builder = new Uri.Builder(); + builder.scheme(protocol) + .authority(server) + .appendPath(path); + Set args = uri.getQueryParameterNames(); + boolean paramFound; + for (String param_name : args) { + paramFound = false; + String param_value = uri.getQueryParameter(param_name); + if (param_value != null) + switch (param_value) { + case "${url}": + paramFound = true; + builder.appendQueryParameter(param_name, bundle_url); + break; + case "${title}": + paramFound = true; + builder.appendQueryParameter(param_name, title); + break; + case "${source}": + paramFound = true; + builder.appendQueryParameter(param_name, bundle_source); + break; + case "${id}": + paramFound = true; + builder.appendQueryParameter(param_name, bundle_id); + break; + case "${description}": + paramFound = true; + builder.appendQueryParameter(param_name, bundle_content); + break; + case "${keywords}": + paramFound = true; + builder.appendQueryParameter(param_name, keywords); + break; + case "${creator}": + paramFound = true; + builder.appendQueryParameter(param_name, bundle_creator); + break; + case "${thumbnailurl}": + paramFound = true; + builder.appendQueryParameter(param_name, bundle_thumbnailurl); + break; + } + if (!paramFound) { + builder.appendQueryParameter(param_name, param_value); + } + } + return builder.build().toString(); + } + + + private String formatedContent(String content, List emojis) { + //Avoid null content + if (content == null) + return ""; + if (emojis == null || emojis.size() == 0) + return content; + for (Emoji emoji : emojis) { + content = content.replaceAll(":" + emoji.shortcode + ":", "" + emoji.shortcode + ""); + } + return content; + } + +} diff --git a/app/src/main/java/app/fedilab/android/activities/DraftActivity.java b/app/src/main/java/app/fedilab/android/activities/DraftActivity.java new file mode 100644 index 000000000..bc93cd930 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/activities/DraftActivity.java @@ -0,0 +1,213 @@ +package app.fedilab.android.activities; +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AlertDialog; +import androidx.lifecycle.ViewModelProvider; +import androidx.recyclerview.widget.LinearLayoutManager; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +import app.fedilab.android.BaseMainActivity; +import app.fedilab.android.R; +import app.fedilab.android.client.entities.StatusDraft; +import app.fedilab.android.client.mastodon.entities.Attachment; +import app.fedilab.android.client.mastodon.entities.Status; +import app.fedilab.android.databinding.ActivityDraftsBinding; +import app.fedilab.android.exception.DBException; +import app.fedilab.android.helper.Helper; +import app.fedilab.android.helper.ThemeHelper; +import app.fedilab.android.ui.drawer.StatusDraftAdapter; +import app.fedilab.android.viewmodel.mastodon.TimelinesVM; + +public class DraftActivity extends BaseActivity implements StatusDraftAdapter.DraftActions { + + + private ActivityDraftsBinding binding; + private List statusDrafts; + private StatusDraftAdapter statusDraftAdapter; + private TimelinesVM timelinesVM; + private LinearLayoutManager mLayoutManager; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + ThemeHelper.applyTheme(this); + binding = ActivityDraftsBinding.inflate(getLayoutInflater()); + setContentView(binding.getRoot()); + setSupportActionBar(binding.toolbar); + ActionBar actionBar = getSupportActionBar(); + //Remove title + if (actionBar != null) { + actionBar.setDisplayShowTitleEnabled(false); + } + binding.toolbar.setPopupTheme(Helper.popupStyle()); + binding.title.setText(R.string.drafts); + binding.loader.setVisibility(View.VISIBLE); + binding.lvStatus.setVisibility(View.GONE); + binding.noAction.setVisibility(View.GONE); + if (getSupportActionBar() != null) { + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + getSupportActionBar().setDisplayShowHomeEnabled(true); + } + timelinesVM = new ViewModelProvider(DraftActivity.this).get(TimelinesVM.class); + timelinesVM.getDrafts(BaseMainActivity.accountWeakReference.get()) + .observe(DraftActivity.this, this::initializeDraftView); + } + + @Override + public boolean onCreateOptionsMenu(@NonNull Menu menu) { + // Inflate the menu; this adds items to the action bar if it is present. + getMenuInflater().inflate(R.menu.menu_draft, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == android.R.id.home) { + finish(); + return true; + } else if (item.getItemId() == R.id.action_delete) { + AlertDialog.Builder unfollowConfirm = new AlertDialog.Builder(DraftActivity.this, Helper.dialogStyle()); + unfollowConfirm.setTitle(getString(R.string.delete_all)); + unfollowConfirm.setMessage(getString(R.string.remove_draft)); + unfollowConfirm.setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss()); + unfollowConfirm.setPositiveButton(R.string.delete, (dialog, which) -> { + new Thread(() -> { + if (statusDrafts != null) { + for (StatusDraft statusDraft : statusDrafts) { + //Check if there are media in the drafts + List attachments = new ArrayList<>(); + if (statusDraft.statusDraftList != null) { + for (Status drafts : statusDraft.statusDraftList) { + if (drafts.media_attachments != null && drafts.media_attachments.size() > 0) { + attachments.addAll(drafts.media_attachments); + } + } + } + //If there are media, we need to remove them first. + if (attachments.size() > 0) { + for (Attachment attachment : attachments) { + if (attachment.local_path != null) { + File fileToDelete = new File(attachment.local_path); + if (fileToDelete.exists()) { + //noinspection ResultOfMethodCallIgnored + fileToDelete.delete(); + } + } + } + } + } + try { + //Delete the draft + new StatusDraft(DraftActivity.this).removeAllDraft(); + Handler mainHandler = new Handler(Looper.getMainLooper()); + Runnable myRunnable = () -> statusDraftAdapter.draftActions.onAllDeleted(); + mainHandler.post(myRunnable); + } catch (DBException e) { + e.printStackTrace(); + } + } + }).start(); + dialog.dismiss(); + }); + unfollowConfirm.show(); + return true; + } + return true; + } + + + /** + * Intialize the view for drafts + * + * @param statusDrafts {@link List} + */ + private void initializeDraftView(List statusDrafts) { + if (statusDrafts == null) { + statusDrafts = new ArrayList<>(); + } + binding.loader.setVisibility(View.GONE); + if (statusDrafts.size() > 0) { + binding.lvStatus.setVisibility(View.VISIBLE); + this.statusDrafts = statusDrafts; + statusDraftAdapter = new StatusDraftAdapter(this.statusDrafts); + statusDraftAdapter.draftActions = this; + mLayoutManager = new LinearLayoutManager(DraftActivity.this); + binding.lvStatus.setLayoutManager(mLayoutManager); + binding.lvStatus.setAdapter(statusDraftAdapter); + } else { + binding.noAction.setVisibility(View.VISIBLE); + } + } + + + @Override + protected void onResume() { + super.onResume(); + //We need to check if drafts changed (ie when coming back from the compose activity) + if (statusDrafts != null && timelinesVM != null) { + timelinesVM.getDrafts(BaseMainActivity.accountWeakReference.get()) + .observe(DraftActivity.this, this::updateDrafts); + } + } + + private void updateDrafts(List statusDrafts) { + if (statusDrafts == null) { + statusDrafts = new ArrayList<>(); + } + int currentPosition = mLayoutManager.findFirstVisibleItemPosition(); + if (statusDrafts.size() > 0) { + int count = this.statusDrafts.size(); + this.statusDrafts.clear(); + this.statusDrafts = new ArrayList<>(); + statusDraftAdapter.notifyItemRangeRemoved(0, count); + this.statusDrafts = statusDrafts; + statusDraftAdapter = new StatusDraftAdapter(this.statusDrafts); + statusDraftAdapter.draftActions = this; + mLayoutManager = new LinearLayoutManager(DraftActivity.this); + binding.lvStatus.setLayoutManager(mLayoutManager); + binding.lvStatus.setAdapter(statusDraftAdapter); + if (currentPosition < this.statusDrafts.size()) { + binding.lvStatus.scrollToPosition(currentPosition); + } + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + binding.lvStatus.setAdapter(null); + binding = null; + } + + @Override + public void onAllDeleted() { + binding.lvStatus.setVisibility(View.GONE); + binding.noAction.setVisibility(View.VISIBLE); + } +} diff --git a/app/src/main/java/app/fedilab/android/activities/EditProfileActivity.java b/app/src/main/java/app/fedilab/android/activities/EditProfileActivity.java new file mode 100644 index 000000000..b38b84665 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/activities/EditProfileActivity.java @@ -0,0 +1,291 @@ +package app.fedilab.android.activities; +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import static app.fedilab.android.BaseMainActivity.instanceInfo; + +import android.content.Intent; +import android.os.Build; +import android.os.Bundle; +import android.provider.MediaStore; +import android.text.Html; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.lifecycle.ViewModelProvider; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; + +import com.bumptech.glide.Glide; +import com.google.android.material.textfield.TextInputEditText; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; + +import app.fedilab.android.BaseMainActivity; +import app.fedilab.android.R; +import app.fedilab.android.client.mastodon.entities.Account; +import app.fedilab.android.client.mastodon.entities.Field; +import app.fedilab.android.databinding.AccountFieldItemBinding; +import app.fedilab.android.databinding.ActivityEditProfileBinding; +import app.fedilab.android.helper.Helper; +import app.fedilab.android.helper.MastodonHelper; +import app.fedilab.android.helper.ThemeHelper; +import app.fedilab.android.viewmodel.mastodon.AccountsVM; +import es.dmoral.toasty.Toasty; + +public class EditProfileActivity extends BaseActivity { + + public static final int PICK_MEDIA_AVATAR = 5705; + public static final int PICK_MEDIA_HEADER = 5706; + private ActivityEditProfileBinding binding; + private AccountsVM accountsVM; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + ThemeHelper.applyThemeBar(this); + binding = ActivityEditProfileBinding.inflate(getLayoutInflater()); + setContentView(binding.getRoot()); + if (getSupportActionBar() != null) + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + + new ViewModelProvider(EditProfileActivity.this).get(AccountsVM.class).getConnectedAccount(BaseMainActivity.currentInstance, BaseMainActivity.currentToken) + .observe(EditProfileActivity.this, account -> { + BaseMainActivity.accountWeakReference.get().mastodon_account = account; + initializeView(); + }); + } + + + @SuppressWarnings("deprecation") + private void initializeView() { + //Hydrate values + MastodonHelper.loadProfileMediaMastodon(binding.bannerPp, BaseMainActivity.accountWeakReference.get().mastodon_account, MastodonHelper.MediaAccountType.HEADER); + MastodonHelper.loadPPMastodon(binding.accountPp, BaseMainActivity.accountWeakReference.get().mastodon_account); + binding.displayName.setText(BaseMainActivity.accountWeakReference.get().mastodon_account.display_name); + binding.acct.setText(String.format(Locale.getDefault(), "%s@%s", BaseMainActivity.accountWeakReference.get().mastodon_account.acct, BaseMainActivity.currentInstance)); + String bio; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + bio = Html.fromHtml(BaseMainActivity.accountWeakReference.get().mastodon_account.note, Html.FROM_HTML_MODE_LEGACY).toString(); + else + bio = Html.fromHtml(BaseMainActivity.accountWeakReference.get().mastodon_account.note).toString(); + binding.bio.setText(bio); + binding.sensitive.setChecked(BaseMainActivity.accountWeakReference.get().mastodon_account.source.sensitive); + binding.bot.setChecked(BaseMainActivity.accountWeakReference.get().mastodon_account.bot); + binding.discoverable.setChecked(BaseMainActivity.accountWeakReference.get().mastodon_account.discoverable); + switch (BaseMainActivity.accountWeakReference.get().mastodon_account.source.privacy) { + case "public": + binding.visibilityPublic.setChecked(true); + break; + case "unlisted": + binding.visibilityUnlisted.setChecked(true); + break; + case "private": + binding.visibilityPrivate.setChecked(true); + break; + case "direct": + binding.visibilityDirect.setChecked(true); + break; + } + if (BaseMainActivity.accountWeakReference.get().mastodon_account.locked) { + binding.locked.setChecked(true); + } else { + binding.unlocked.setChecked(true); + } + List fields = BaseMainActivity.accountWeakReference.get().mastodon_account.fields; + if (fields != null && fields.size() > 0) { + for (Field field : fields) { + AccountFieldItemBinding fieldItemBinding = AccountFieldItemBinding.inflate(getLayoutInflater()); + fieldItemBinding.name.setText(field.name); + String value; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + value = Html.fromHtml(field.value, Html.FROM_HTML_MODE_LEGACY).toString(); + else + value = Html.fromHtml(field.value).toString(); + fieldItemBinding.value.setText(value); + fieldItemBinding.remove.setOnClickListener(v -> { + AlertDialog.Builder deleteConfirm = new AlertDialog.Builder(EditProfileActivity.this, Helper.dialogStyle()); + deleteConfirm.setTitle(getString(R.string.delete_field)); + deleteConfirm.setMessage(getString(R.string.delete_field_confirm)); + deleteConfirm.setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss()); + deleteConfirm.setPositiveButton(R.string.delete, (dialog, which) -> { + binding.fieldsContainer.removeView(fieldItemBinding.getRoot()); + if (binding.fieldsContainer.getChildCount() < 4) { + binding.fieldsContainer.setVisibility(View.VISIBLE); + } else { + binding.fieldsContainer.setVisibility(View.GONE); + } + dialog.dismiss(); + }); + deleteConfirm.create().show(); + }); + binding.fieldsContainer.addView(fieldItemBinding.getRoot()); + } + + } + binding.addField.setOnClickListener(view -> { + AccountFieldItemBinding fieldItemBinding = AccountFieldItemBinding.inflate(getLayoutInflater()); + fieldItemBinding.remove.setOnClickListener(v -> { + AlertDialog.Builder deleteConfirm = new AlertDialog.Builder(EditProfileActivity.this, Helper.dialogStyle()); + deleteConfirm.setTitle(getString(R.string.delete_field)); + deleteConfirm.setMessage(getString(R.string.delete_field_confirm)); + deleteConfirm.setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss()); + deleteConfirm.setPositiveButton(R.string.delete, (dialog, which) -> { + binding.fieldsContainer.removeView(fieldItemBinding.getRoot()); + if (binding.fieldsContainer.getChildCount() < 4) { + binding.fieldsContainer.setVisibility(View.VISIBLE); + } else { + binding.fieldsContainer.setVisibility(View.GONE); + } + dialog.dismiss(); + }); + deleteConfirm.create().show(); + }); + binding.fieldsContainer.addView(fieldItemBinding.getRoot()); + if (binding.fieldsContainer.getChildCount() >= 4) { + binding.addField.setVisibility(View.GONE); + } + }); + //Actions with the activity + accountsVM = new ViewModelProvider(EditProfileActivity.this).get(AccountsVM.class); + binding.headerSelect.setOnClickListener(view -> startActivityForResult(prepareIntent(), EditProfileActivity.PICK_MEDIA_AVATAR)); + + binding.avatarSelect.setOnClickListener(view -> startActivityForResult(prepareIntent(), EditProfileActivity.PICK_MEDIA_HEADER)); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (requestCode == PICK_MEDIA_AVATAR && resultCode == RESULT_OK) { + binding.avatarProgress.setVisibility(View.VISIBLE); + Glide.with(EditProfileActivity.this) + .asDrawable() + .load(data.getData()) + .thumbnail(0.1f) + .into(binding.accountPp); + accountsVM.updateProfilePicture(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, data.getData(), AccountsVM.UpdateMediaType.AVATAR) + .observe(EditProfileActivity.this, account -> { + sendBroadCast(account); + binding.avatarProgress.setVisibility(View.GONE); + BaseMainActivity.accountWeakReference.get().mastodon_account = account; + }); + } else if (requestCode == PICK_MEDIA_HEADER && resultCode == RESULT_OK) { + Glide.with(EditProfileActivity.this) + .asDrawable() + .load(data.getData()) + .thumbnail(0.1f) + .into(binding.bannerPp); + binding.headerProgress.setVisibility(View.VISIBLE); + accountsVM.updateProfilePicture(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, data.getData(), AccountsVM.UpdateMediaType.HEADER) + .observe(EditProfileActivity.this, account -> { + sendBroadCast(account); + binding.headerProgress.setVisibility(View.GONE); + BaseMainActivity.accountWeakReference.get().mastodon_account = account; + }); + } + } + + private void sendBroadCast(Account account) { + Bundle b = new Bundle(); + b.putBoolean(Helper.RECEIVE_REDRAW_PROFILE, true); + b.putSerializable(Helper.ARG_ACCOUNT, account); + Intent intentBD = new Intent(Helper.BROADCAST_DATA); + intentBD.putExtras(b); + LocalBroadcastManager.getInstance(EditProfileActivity.this).sendBroadcast(intentBD); + } + + private Intent prepareIntent() { + Intent intent; + intent = new Intent(Intent.ACTION_GET_CONTENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setType("*/*"); + String[] mimetypes; + long max_size = -1; + if (instanceInfo.getMimeTypeImage().size() > 0) { + mimetypes = instanceInfo.getMimeTypeImage().toArray(new String[0]); + max_size = instanceInfo.configuration.media_attachments.image_size_limit; + } else { + mimetypes = new String[]{"image/*"}; + } + if (max_size > 0) { + intent.putExtra(MediaStore.EXTRA_SIZE_LIMIT, max_size); + } + intent.putExtra(Intent.EXTRA_MIME_TYPES, mimetypes); + return intent; + } + + @Override + public boolean onCreateOptionsMenu(@NonNull Menu menu) { + getMenuInflater().inflate(R.menu.menu_edit_profile, menu); + return super.onCreateOptionsMenu(menu); + } + + private String getPrivacy() { + if (binding.visibilityPublic.isChecked()) { + return "public"; + } else if (binding.visibilityUnlisted.isChecked()) { + return "unlisted"; + } else if (binding.visibilityPrivate.isChecked()) { + return "private"; + } else if (binding.visibilityDirect.isChecked()) { + return "direct"; + } + return null; + } + + LinkedHashMap getFields() { + LinkedHashMap fields = new LinkedHashMap<>(); + for (int i = 0; i < binding.fieldsContainer.getChildCount(); i++) { + Field.FieldParams field = new Field.FieldParams(); + field.name = ((TextInputEditText) binding.fieldsContainer.getChildAt(i).findViewById(R.id.name)).getText().toString().trim(); + field.value = ((TextInputEditText) binding.fieldsContainer.getChildAt(i).findViewById(R.id.value)).getText().toString().trim(); + fields.put(i, field); + } + return fields; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == android.R.id.home) { + finish(); + return true; + } else if (item.getItemId() == R.id.action_save) { + accountsVM.updateCredentials(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, + binding.discoverable.isChecked(), + binding.bot.isChecked(), + binding.displayName.getText().toString().trim(), + binding.bio.getText().toString(), + binding.locked.isChecked(), + getPrivacy(), + binding.sensitive.isChecked(), + null, + getFields() + ) + .observe(EditProfileActivity.this, account -> { + BaseMainActivity.accountWeakReference.get().mastodon_account = account; + sendBroadCast(account); + Toasty.success(EditProfileActivity.this, getString(R.string.profiled_updated), Toasty.LENGTH_LONG).show(); + finish(); + }); + return true; + } + return super.onOptionsItemSelected(item); + } +} diff --git a/app/src/main/java/app/fedilab/android/activities/FilterActivity.java b/app/src/main/java/app/fedilab/android/activities/FilterActivity.java new file mode 100644 index 000000000..27d52b0e6 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/activities/FilterActivity.java @@ -0,0 +1,238 @@ +package app.fedilab.android.activities; +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import android.content.Context; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.WindowManager; +import android.view.inputmethod.InputMethodManager; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.Button; + +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.ViewModelProvider; +import androidx.lifecycle.ViewModelStoreOwner; +import androidx.recyclerview.widget.LinearLayoutManager; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import app.fedilab.android.BaseMainActivity; +import app.fedilab.android.R; +import app.fedilab.android.client.mastodon.entities.Filter; +import app.fedilab.android.databinding.ActivityFiltersBinding; +import app.fedilab.android.databinding.PopupAddFilterBinding; +import app.fedilab.android.helper.Helper; +import app.fedilab.android.helper.ThemeHelper; +import app.fedilab.android.ui.drawer.FilterAdapter; +import app.fedilab.android.viewmodel.mastodon.AccountsVM; + +public class FilterActivity extends BaseActivity implements FilterAdapter.Delete { + + private ActivityFiltersBinding binding; + private List filterList; + private FilterAdapter filterAdapter; + + /** + * Method that allows to add or edit filter depending if Filter passing into params is null (null = insertion) + * + * @param context - Context + * @param filter - {@link Filter} + * @param listener - {@link FilterAdapter.FilterAction} + */ + public static void addEditFilter(Context context, Filter filter, FilterAdapter.FilterAction listener) { + AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(context, Helper.dialogStyle()); + PopupAddFilterBinding popupAddFilterBinding = PopupAddFilterBinding.inflate(LayoutInflater.from(context)); + AccountsVM accountsVM = new ViewModelProvider((ViewModelStoreOwner) context).get(AccountsVM.class); + dialogBuilder.setView(popupAddFilterBinding.getRoot()); + ArrayAdapter adapterResize = ArrayAdapter.createFromResource(Objects.requireNonNull(context), + R.array.filter_expire, android.R.layout.simple_spinner_dropdown_item); + popupAddFilterBinding.filterExpire.setAdapter(adapterResize); + final int[] expire = {-1}; + popupAddFilterBinding.filterExpire.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent1, View view, int position1, long id) { + switch (position1) { + case 0: + expire[0] = -1; + break; + case 1: + expire[0] = 3600; + break; + case 2: + expire[0] = 21600; + break; + case 3: + expire[0] = 43200; + break; + case 4: + expire[0] = 86400; + break; + case 5: + expire[0] = 604800; + break; + } + } + + @Override + public void onNothingSelected(AdapterView parent1) { + } + }); + if (filter != null) { + popupAddFilterBinding.addPhrase.setText(filter.phrase); + if (filter.context != null) + for (String val : filter.context) { + switch (val) { + case "home": + popupAddFilterBinding.contextHome.setChecked(true); + break; + case "public": + popupAddFilterBinding.contextPublic.setChecked(true); + break; + case "notifications": + popupAddFilterBinding.contextNotification.setChecked(true); + break; + case "thread": + popupAddFilterBinding.contextConversation.setChecked(true); + break; + } + } + popupAddFilterBinding.contextWholeWord.setChecked(filter.whole_word); + popupAddFilterBinding.contextDrop.setChecked(filter.irreversible); + } + + + AlertDialog alertDialog = dialogBuilder.setPositiveButton(R.string.validate, null) + .setNegativeButton(R.string.cancel, null).create(); + alertDialog.setOnShowListener(dialogInterface -> { + + Button button = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE); + button.setOnClickListener(view -> { + if (popupAddFilterBinding.addPhrase.getText() == null || popupAddFilterBinding.addPhrase.getText().toString().trim().length() == 0) { + popupAddFilterBinding.addPhrase.setError(context.getString(R.string.cannot_be_empty)); + return; + } + if (!popupAddFilterBinding.contextConversation.isChecked() && !popupAddFilterBinding.contextHome.isChecked() && !popupAddFilterBinding.contextPublic.isChecked() && !popupAddFilterBinding.contextNotification.isChecked()) { + popupAddFilterBinding.contextDescription.setError(context.getString(R.string.cannot_be_empty)); + return; + } + if (popupAddFilterBinding.addPhrase.getText() != null && popupAddFilterBinding.addPhrase.getText().toString().trim().length() > 0) { + Filter filterSent = new Filter(); + ArrayList contextFilter = new ArrayList<>(); + if (popupAddFilterBinding.contextHome.isChecked()) + contextFilter.add("home"); + if (popupAddFilterBinding.contextPublic.isChecked()) + contextFilter.add("public"); + if (popupAddFilterBinding.contextNotification.isChecked()) + contextFilter.add("notifications"); + if (popupAddFilterBinding.contextConversation.isChecked()) + contextFilter.add("thread"); + filterSent.context = contextFilter; + filterSent.expires_at_sent = expire[0]; + filterSent.phrase = popupAddFilterBinding.addPhrase.getText().toString(); + filterSent.whole_word = popupAddFilterBinding.contextWholeWord.isChecked(); + filterSent.irreversible = popupAddFilterBinding.contextDrop.isChecked(); + if (filter != null) { + accountsVM.editFilter(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, filter.id, filterSent.phrase, filterSent.context, filterSent.irreversible, filterSent.whole_word, filterSent.expires_at_sent) + .observe((LifecycleOwner) context, listener::callback); + } else { + accountsVM.addFilter(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, filterSent.phrase, filterSent.context, filterSent.irreversible, filterSent.whole_word, filterSent.expires_at_sent) + .observe((LifecycleOwner) context, listener::callback); + } + alertDialog.dismiss(); + } + }); + Button buttonCancel = alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE); + buttonCancel.setOnClickListener(view -> alertDialog.dismiss()); + }); + alertDialog.setTitle(context.getString(R.string.action_update_filter)); + alertDialog.setOnDismissListener(dialogInterface -> { + //Hide keyboard + InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(popupAddFilterBinding.addPhrase.getWindowToken(), 0); + }); + if (alertDialog.getWindow() != null) { + alertDialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE); + } + alertDialog.show(); + + } + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + ThemeHelper.applyThemeBar(this); + binding = ActivityFiltersBinding.inflate(getLayoutInflater()); + setContentView(binding.getRoot()); + filterList = new ArrayList<>(); + if (getSupportActionBar() != null) + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + + AccountsVM accountsVM = new ViewModelProvider(FilterActivity.this).get(AccountsVM.class); + accountsVM.getFilters(BaseMainActivity.currentInstance, BaseMainActivity.currentToken) + .observe(FilterActivity.this, filters -> { + BaseMainActivity.mainFilters = filters; + if (filters != null && filters.size() > 0) { + filterList.addAll(filters); + filterAdapter = new FilterAdapter(filterList); + filterAdapter.delete = this; + binding.lvFilters.setAdapter(filterAdapter); + binding.lvFilters.setLayoutManager(new LinearLayoutManager(FilterActivity.this)); + } else { + binding.lvFilters.setVisibility(View.GONE); + binding.noAction.setVisibility(View.VISIBLE); + } + }); + + binding.addFilter.setOnClickListener(v -> addEditFilter(FilterActivity.this, null, filter -> { + if (filter != null) { + filterList.add(0, filter); + if (filterAdapter != null) { + filterAdapter.notifyItemInserted(0); + } else { + filterAdapter = new FilterAdapter(filterList); + filterAdapter.delete = FilterActivity.this; + binding.lvFilters.setAdapter(filterAdapter); + binding.lvFilters.setLayoutManager(new LinearLayoutManager(FilterActivity.this)); + } + binding.lvFilters.setVisibility(View.VISIBLE); + binding.noAction.setVisibility(View.GONE); + } + })); + + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == android.R.id.home) { + finish(); + return true; + } + return super.onOptionsItemSelected(item); + } + + @Override + public void allFiltersDeleted() { + binding.lvFilters.setVisibility(View.GONE); + binding.noAction.setVisibility(View.VISIBLE); + } +} diff --git a/app/src/main/java/app/fedilab/android/activities/HashTagActivity.java b/app/src/main/java/app/fedilab/android/activities/HashTagActivity.java new file mode 100644 index 000000000..585f0bca3 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/activities/HashTagActivity.java @@ -0,0 +1,162 @@ +package app.fedilab.android.activities; +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + + +import android.content.Intent; +import android.os.Bundle; +import android.view.Menu; +import android.view.MenuItem; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.ActionBar; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; + +import java.util.ArrayList; +import java.util.List; + +import app.fedilab.android.BaseMainActivity; +import app.fedilab.android.R; +import app.fedilab.android.client.entities.Pinned; +import app.fedilab.android.client.entities.StatusDraft; +import app.fedilab.android.client.entities.Timeline; +import app.fedilab.android.client.entities.app.PinnedTimeline; +import app.fedilab.android.client.entities.app.TagTimeline; +import app.fedilab.android.client.mastodon.entities.Status; +import app.fedilab.android.databinding.ActivityHashtagBinding; +import app.fedilab.android.exception.DBException; +import app.fedilab.android.helper.Helper; +import app.fedilab.android.helper.ThemeHelper; +import app.fedilab.android.ui.fragment.timeline.FragmentMastodonTimeline; +import es.dmoral.toasty.Toasty; + + +public class HashTagActivity extends BaseActivity { + + + public static int position; + private String tag; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + ThemeHelper.applyTheme(this); + ActivityHashtagBinding binding = ActivityHashtagBinding.inflate(getLayoutInflater()); + + setContentView(binding.getRoot()); + Bundle b = getIntent().getExtras(); + if (b != null) { + tag = b.getString(Helper.ARG_SEARCH_KEYWORD, null); + } + if (tag == null) + finish(); + + setSupportActionBar(binding.toolbar); + ActionBar actionBar = getSupportActionBar(); + //Remove title + if (actionBar != null) { + actionBar.setDisplayShowTitleEnabled(false); + } + binding.title.setText(tag); + if (getSupportActionBar() != null) { + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + getSupportActionBar().setDisplayShowHomeEnabled(true); + } + + Bundle bundle = new Bundle(); + bundle.putSerializable(Helper.ARG_TIMELINE_TYPE, Timeline.TimeLineEnum.TAG); + bundle.putString(Helper.ARG_SEARCH_KEYWORD, tag); + Helper.addFragment(getSupportFragmentManager(), R.id.nav_host_fragment_tags, new FragmentMastodonTimeline(), bundle, null, null); + binding.toolbar.setPopupTheme(Helper.popupStyle()); + binding.compose.setOnClickListener(v -> { + Intent intentToot = new Intent(HashTagActivity.this, ComposeActivity.class); + StatusDraft statusDraft = new StatusDraft(); + Status status = new Status(); + status.text = "#" + tag; + List statuses = new ArrayList<>(); + statuses.add(status); + statusDraft.statusDraftList = statuses; + Bundle _b = new Bundle(); + _b.putSerializable(Helper.ARG_TAG_TIMELINE, statusDraft); + intentToot.putExtras(_b); + startActivity(intentToot); + }); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == android.R.id.home) { + finish(); + return true; + } else if (item.getItemId() == R.id.action_add_timeline) { + new Thread(() -> { + try { + Pinned pinned = new Pinned(HashTagActivity.this).getPinned(BaseMainActivity.accountWeakReference.get()); + boolean canBeAdded = true; + boolean update = true; + if (pinned == null) { + pinned = new Pinned(); + pinned.pinnedTimelines = new ArrayList<>(); + update = false; + } else { + for (PinnedTimeline pinnedTimeline : pinned.pinnedTimelines) { + if (pinnedTimeline.type == Timeline.TimeLineEnum.TAG) { + if (pinnedTimeline.tagTimeline.name.compareTo(tag.trim()) == 0) { + canBeAdded = false; + } + } + } + } + if (!canBeAdded) { + Toasty.warning(HashTagActivity.this, getString(R.string.tags_already_stored), Toasty.LENGTH_SHORT).show(); + return; + } + PinnedTimeline pinnedTimeline = new PinnedTimeline(); + pinnedTimeline.type = Timeline.TimeLineEnum.TAG; + pinnedTimeline.position = pinned.pinnedTimelines.size(); + pinnedTimeline.displayed = true; + TagTimeline tagTimeline = new TagTimeline(); + tagTimeline.name = tag.trim(); + tagTimeline.isNSFW = false; + tagTimeline.isART = false; + pinnedTimeline.tagTimeline = tagTimeline; + pinned.pinnedTimelines.add(pinnedTimeline); + if (update) { + new Pinned(HashTagActivity.this).updatePinned(pinned); + } else { + new Pinned(HashTagActivity.this).insertPinned(pinned); + } + Bundle b = new Bundle(); + b.putBoolean(Helper.RECEIVE_REDRAW_TOPBAR, true); + Intent intentBD = new Intent(Helper.BROADCAST_DATA); + intentBD.putExtras(b); + LocalBroadcastManager.getInstance(HashTagActivity.this).sendBroadcast(intentBD); + } catch (DBException e) { + e.printStackTrace(); + } + }).start(); + } + + return super.onOptionsItemSelected(item); + } + + + @Override + public boolean onCreateOptionsMenu(@NonNull Menu menu) { + getMenuInflater().inflate(R.menu.menu_reorder, menu); + return super.onCreateOptionsMenu(menu); + } + +} diff --git a/app/src/main/java/app/fedilab/android/activities/InstanceActivity.java b/app/src/main/java/app/fedilab/android/activities/InstanceActivity.java new file mode 100644 index 000000000..3e23b5a60 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/activities/InstanceActivity.java @@ -0,0 +1,109 @@ +package app.fedilab.android.activities; +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + + +import android.content.Intent; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.text.Html; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Toast; + +import androidx.lifecycle.ViewModelProvider; + +import com.bumptech.glide.Glide; + +import app.fedilab.android.BaseMainActivity; +import app.fedilab.android.R; +import app.fedilab.android.client.mastodon.entities.Instance; +import app.fedilab.android.databinding.ActivityInstanceBinding; +import app.fedilab.android.helper.ThemeHelper; +import app.fedilab.android.viewmodel.mastodon.InstancesVM; +import es.dmoral.toasty.Toasty; + + +public class InstanceActivity extends BaseActivity { + + + ActivityInstanceBinding binding; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + ThemeHelper.applyThemeDialog(this); + binding = ActivityInstanceBinding.inflate(getLayoutInflater()); + + setContentView(binding.getRoot()); + + getWindow().setLayout(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); + if (getSupportActionBar() != null) + getSupportActionBar().hide(); + + binding.close.setOnClickListener(view -> finish()); + + if (getSupportActionBar() != null) { + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + getSupportActionBar().setDisplayShowHomeEnabled(true); + } + + InstancesVM instancesVM = new ViewModelProvider(InstanceActivity.this).get(InstancesVM.class); + instancesVM.getInstance(BaseMainActivity.currentInstance).observe(InstanceActivity.this, instanceInfo -> { + binding.instanceContainer.setVisibility(View.VISIBLE); + binding.loader.setVisibility(View.GONE); + if (instanceInfo == null || instanceInfo.info == null) { + Toasty.error(InstanceActivity.this, getString(R.string.toast_error), Toast.LENGTH_LONG).show(); + return; + } + Instance instance = instanceInfo.info; + binding.instanceTitle.setText(instance.title); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + binding.instanceDescription.setText(Html.fromHtml(instance.description, Html.FROM_HTML_MODE_LEGACY)); + else + binding.instanceDescription.setText(Html.fromHtml(instance.description)); + if (instance.description == null || instance.description.trim().length() == 0) + binding.instanceDescription.setText(getString(R.string.instance_no_description)); + binding.instanceVersion.setText(instance.version); + binding.instanceUri.setText(instance.uri); + if (instance.email == null) { + binding.instanceContact.hide(); + } + Glide.with(InstanceActivity.this) + .asBitmap() + .load(instance.thumbnail) + .into(binding.backGroundImage); + + binding.instanceContact.setOnClickListener(v -> { + Intent emailIntent = new Intent(Intent.ACTION_SENDTO, Uri.fromParts("mailto", instance.email, null)); + emailIntent.putExtra(Intent.EXTRA_SUBJECT, "[Mastodon] - " + instance.uri); + startActivity(Intent.createChooser(emailIntent, getString(R.string.send_email))); + }); + }); + } + + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == android.R.id.home) { + finish(); + return true; + } + return super.onOptionsItemSelected(item); + } + +} diff --git a/app/src/main/java/app/fedilab/android/activities/InstanceHealthActivity.java b/app/src/main/java/app/fedilab/android/activities/InstanceHealthActivity.java new file mode 100644 index 000000000..ad49bba43 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/activities/InstanceHealthActivity.java @@ -0,0 +1,114 @@ +package app.fedilab.android.activities; +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + + +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.text.SpannableString; +import android.text.style.UnderlineSpan; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; + +import androidx.core.content.ContextCompat; +import androidx.lifecycle.ViewModelProvider; + +import com.bumptech.glide.Glide; + +import app.fedilab.android.BaseMainActivity; +import app.fedilab.android.R; +import app.fedilab.android.client.entities.InstanceSocial; +import app.fedilab.android.databinding.ActivityInstanceSocialBinding; +import app.fedilab.android.helper.Helper; +import app.fedilab.android.helper.ThemeHelper; +import app.fedilab.android.viewmodel.mastodon.InstanceSocialVM; + + +public class InstanceHealthActivity extends BaseActivity { + + private ActivityInstanceSocialBinding binding; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + ThemeHelper.applyThemeDialog(this); + binding = ActivityInstanceSocialBinding.inflate(getLayoutInflater()); + + setContentView(binding.getRoot()); + getWindow().setLayout(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); + if (getSupportActionBar() != null) + getSupportActionBar().hide(); + + binding.close.setOnClickListener(view -> finish()); + + SpannableString content = new SpannableString(binding.refInstance.getText().toString()); + content.setSpan(new UnderlineSpan(), 0, content.length(), 0); + binding.refInstance.setText(content); + binding.refInstance.setOnClickListener(view -> { + Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("https://instances.social")); + startActivity(browserIntent); + }); + + checkInstance(); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == android.R.id.home) { + finish(); + return true; + } + return super.onOptionsItemSelected(item); + } + + + private void checkInstance() { + + + InstanceSocialVM instanceSocialVM = new ViewModelProvider(InstanceHealthActivity.this).get(InstanceSocialVM.class); + instanceSocialVM.getInstances(BaseMainActivity.currentInstance.trim()).observe(InstanceHealthActivity.this, instanceSocialList -> { + if (instanceSocialList != null && instanceSocialList.instances.size() > 0) { + InstanceSocial.Instance instanceSocial = instanceSocialList.instances.get(0); + if (instanceSocial.thumbnail != null && !instanceSocial.thumbnail.equals("null")) + Glide.with(InstanceHealthActivity.this) + .asBitmap() + .load(instanceSocial.thumbnail) + .into(binding.backGroundImage); + binding.name.setText(instanceSocial.name); + if (instanceSocial.up) { + binding.up.setText(R.string.is_up); + binding.up.setTextColor(ContextCompat.getColor(InstanceHealthActivity.this, R.color.green_1)); + } else { + binding.up.setText(R.string.is_down); + binding.up.setTextColor(ContextCompat.getColor(InstanceHealthActivity.this, R.color.red_1)); + } + binding.uptime.setText(getString(R.string.instance_health_uptime, (instanceSocial.uptime * 100))); + if (instanceSocial.checked_at != null) + binding.checkedAt.setText(getString(R.string.instance_health_checkedat, Helper.dateToString(instanceSocial.checked_at))); + binding.values.setText(getString(R.string.instance_health_indication, instanceSocial.version, Helper.withSuffix(instanceSocial.active_users), Helper.withSuffix(instanceSocial.statuses))); + binding.instanceContainer.setVisibility(View.VISIBLE); + } else { + binding.instanceContainer.setVisibility(View.VISIBLE); + binding.mainContainer.setVisibility(View.GONE); + binding.noInstance.setVisibility(View.VISIBLE); + } + binding.loader.setVisibility(View.GONE); + }); + } + + +} diff --git a/app/src/main/java/app/fedilab/android/activities/InstanceProfileActivity.java b/app/src/main/java/app/fedilab/android/activities/InstanceProfileActivity.java new file mode 100644 index 000000000..1664f8119 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/activities/InstanceProfileActivity.java @@ -0,0 +1,127 @@ +package app.fedilab.android.activities; +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + + +import static androidx.core.text.HtmlCompat.FROM_HTML_MODE_LEGACY; + +import android.os.Build; +import android.os.Bundle; +import android.text.Html; +import android.text.SpannableString; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.lifecycle.ViewModelProvider; +import androidx.recyclerview.widget.LinearLayoutManager; + +import java.util.ArrayList; +import java.util.List; + +import app.fedilab.android.BaseMainActivity; +import app.fedilab.android.R; +import app.fedilab.android.client.mastodon.entities.Account; +import app.fedilab.android.databinding.ActivityInstanceProfileBinding; +import app.fedilab.android.helper.Helper; +import app.fedilab.android.helper.ThemeHelper; +import app.fedilab.android.ui.drawer.AccountAdapter; +import app.fedilab.android.viewmodel.mastodon.NodeInfoVM; +import app.fedilab.android.viewmodel.mastodon.SearchVM; +import es.dmoral.toasty.Toasty; + +public class InstanceProfileActivity extends BaseActivity { + + + private String instance; + private ActivityInstanceProfileBinding binding; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + ThemeHelper.applyThemeDialog(this); + binding = ActivityInstanceProfileBinding.inflate(getLayoutInflater()); + setContentView(binding.getRoot()); + getWindow().setLayout(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); + Bundle b = getIntent().getExtras(); + if (getSupportActionBar() != null) + getSupportActionBar().hide(); + if (b != null) + instance = b.getString(Helper.ARG_INSTANCE, null); + if (instance == null) { + finish(); + } + Button close = findViewById(R.id.close); + close.setOnClickListener(view -> finish()); + NodeInfoVM nodeInfoVM = new ViewModelProvider(InstanceProfileActivity.this).get(NodeInfoVM.class); + nodeInfoVM.getNodeInfo(instance).observe(InstanceProfileActivity.this, nodeInfo -> { + if (nodeInfo == null) { + Toasty.error(InstanceProfileActivity.this, getString(R.string.toast_error), Toast.LENGTH_LONG).show(); + finish(); + return; + } + binding.name.setText(nodeInfo.metadata != null ? nodeInfo.metadata.nodeName : instance); + SpannableString descriptionSpan; + if (nodeInfo.metadata != null && nodeInfo.metadata.nodeDescription != null) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + descriptionSpan = new SpannableString(Html.fromHtml(nodeInfo.metadata.nodeDescription, FROM_HTML_MODE_LEGACY)); + else + descriptionSpan = new SpannableString(Html.fromHtml(nodeInfo.metadata.nodeDescription)); + binding.description.setText(descriptionSpan, TextView.BufferType.SPANNABLE); + } + binding.userCount.setText(Helper.withSuffix((nodeInfo.usage.users.total))); + binding.statusCount.setText(Helper.withSuffix(((nodeInfo.usage.localPosts)))); + String softwareStr = nodeInfo.software.name + " - "; + binding.software.setText(softwareStr); + binding.version.setText(nodeInfo.software.version); + if (nodeInfo.metadata != null && nodeInfo.metadata.staffAccounts != null && nodeInfo.metadata.staffAccounts.size() > 0) { + SearchVM searchVM = new ViewModelProvider(InstanceProfileActivity.this).get(SearchVM.class); + List accounts = new ArrayList<>(); + for (String accountURL : nodeInfo.metadata.staffAccounts) { + searchVM.search(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, accountURL, null, "accounts", false, true, false, 0, null, null, 1) + .observe(InstanceProfileActivity.this, results -> { + if (results.accounts != null && results.accounts.size() > 0) { + accounts.add(results.accounts.get(0)); + } + if (accounts.size() == nodeInfo.metadata.staffAccounts.size()) { + AccountAdapter accountsListAdapter = new AccountAdapter(accounts); + binding.lvAccounts.setAdapter(accountsListAdapter); + final LinearLayoutManager mLayoutManager; + mLayoutManager = new LinearLayoutManager(InstanceProfileActivity.this); + binding.lvAccounts.setLayoutManager(mLayoutManager); + } + }); + + } + } + binding.instanceContainer.setVisibility(View.VISIBLE); + binding.loader.setVisibility(View.GONE); + }); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == android.R.id.home) { + finish(); + return true; + } + return super.onOptionsItemSelected(item); + } + + +} diff --git a/app/src/main/java/app/fedilab/android/activities/LoginActivity.java b/app/src/main/java/app/fedilab/android/activities/LoginActivity.java new file mode 100644 index 000000000..0d1af1661 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/activities/LoginActivity.java @@ -0,0 +1,222 @@ +package app.fedilab.android.activities; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import static app.fedilab.android.BaseMainActivity.admin; +import static app.fedilab.android.BaseMainActivity.api; +import static app.fedilab.android.BaseMainActivity.client_id; +import static app.fedilab.android.BaseMainActivity.client_secret; +import static app.fedilab.android.BaseMainActivity.currentInstance; +import static app.fedilab.android.BaseMainActivity.software; +import static app.fedilab.android.helper.Helper.PREF_USER_TOKEN; +import static app.fedilab.android.helper.MastodonHelper.REDIRECT_CONTENT_WEB; + +import android.content.Intent; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.view.Menu; +import android.view.MenuItem; +import android.widget.FrameLayout; +import android.widget.Toast; + +import androidx.lifecycle.ViewModelProvider; +import androidx.navigation.fragment.NavHostFragment; +import androidx.preference.PreferenceManager; + +import org.jetbrains.annotations.NotNull; + +import app.fedilab.android.BaseMainActivity; +import app.fedilab.android.R; +import app.fedilab.android.client.entities.Account; +import app.fedilab.android.client.entities.WellKnownNodeinfo; +import app.fedilab.android.exception.DBException; +import app.fedilab.android.helper.Helper; +import app.fedilab.android.helper.ThemeHelper; +import app.fedilab.android.ui.fragment.login.FragmentLoginMain; +import app.fedilab.android.viewmodel.mastodon.AccountsVM; +import app.fedilab.android.viewmodel.mastodon.OauthVM; +import es.dmoral.toasty.Toasty; + + +public class LoginActivity extends BaseActivity { + + + private final int PICK_IMPORT = 5557; + private String oldSearch; + private String autofilledInstance; + private WellKnownNodeinfo.NodeInfo nodeInfo; + private NavHostFragment host; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + ThemeHelper.applyTheme(this); + SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(LoginActivity.this); + setContentView(new FrameLayout(this)); + + Helper.addFragment(getSupportFragmentManager(), android.R.id.content, new FragmentLoginMain(), null, null, null); + + Bundle b = getIntent().getExtras(); + if (b != null) { + autofilledInstance = b.getString("instance", null); + admin = b.getBoolean("admin", false); + } + + + //The activity handles a redirect URI, it will extract token code and will proceed to authentication + //That happens when the user wants to use an external browser + if (getIntent() != null && getIntent().getData() != null && getIntent().getData().toString().contains(REDIRECT_CONTENT_WEB + "?code=")) { + String url = getIntent().getData().toString(); + String[] val = url.split("code="); + if (val.length < 2) { + Toasty.error(LoginActivity.this, getString(R.string.toast_code_error), Toast.LENGTH_LONG).show(); + return; + } + String code = val[1]; + OauthVM oauthVM = new ViewModelProvider(LoginActivity.this).get(OauthVM.class); + //We are dealing with a Mastodon API + if (api == Account.API.MASTODON) { + //API call to get the user token + oauthVM.createToken(currentInstance, "authorization_code", client_id, client_secret, Helper.REDIRECT_CONTENT_WEB, Helper.OAUTH_SCOPES, code) + .observe(LoginActivity.this, tokenObj -> { + Account account = new Account(); + account.client_id = BaseMainActivity.client_id; + account.client_secret = BaseMainActivity.client_secret; + account.token = tokenObj.token_type + " " + tokenObj.access_token; + account.api = api; + account.software = software; + account.instance = currentInstance; + //API call to retrieve account information for the new token + AccountsVM accountsVM = new ViewModelProvider(LoginActivity.this).get(AccountsVM.class); + accountsVM.getConnectedAccount(currentInstance, account.token).observe(LoginActivity.this, mastodonAccount -> { + account.mastodon_account = mastodonAccount; + new Thread(() -> { + try { + account.user_id = mastodonAccount.id; + //update the database + new Account(LoginActivity.this).insertOrUpdate(account); + + BaseMainActivity.currentToken = account.token; + BaseMainActivity.currentUserID = account.user_id; + api = Account.API.MASTODON; + SharedPreferences.Editor editor = sharedpreferences.edit(); + editor.putString(PREF_USER_TOKEN, account.token); + editor.commit(); + //The user is now aut + //The user is now authenticated, it will be redirected to MainActivity + Handler mainHandler = new Handler(Looper.getMainLooper()); + Runnable myRunnable = () -> { + Intent mainActivity = new Intent(LoginActivity.this, BaseMainActivity.class); + startActivity(mainActivity); + finish(); + }; + mainHandler.post(myRunnable); + } catch (DBException e) { + e.printStackTrace(); + } + }).start(); + }); + }); + } + } + } + + @Override + protected void onResume() { + super.onResume(); + } + + + @Override + public boolean onCreateOptionsMenu(@NotNull Menu menu) { + // Inflate the menu; this adds items to the action bar if it is present. + getMenuInflater().inflate(R.menu.main_login, menu); + SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(LoginActivity.this); + boolean embedded_browser = sharedpreferences.getBoolean(getString(R.string.SET_EMBEDDED_BROWSER), true); + menu.findItem(R.id.action_custom_tabs).setChecked(!embedded_browser); + /* boolean security_provider = sharedpreferences.getBoolean(getString(R.string.SET_SECURITY_PROVIDER), true); + menu.findItem(R.id.action_provider).setChecked(security_provider);*/ + return true; + } + + @Override + public boolean onOptionsItemSelected(@NotNull MenuItem item) { + // Handle action bar item clicks here. The action bar will + // automatically handle clicks on the Home/Up button, so long + // as you specify a parent activity in AndroidManifest.xml. + int id = item.getItemId(); + + //noinspection SimplifiableIfStatement + if (id == R.id.action_about) { + // Intent intent = new Intent(LoginActivity.this, AboutActivity.class); + // startActivity(intent); + } else if (id == R.id.action_privacy) { + // Intent intent = new Intent(LoginActivity.this, PrivacyActivity.class); + // startActivity(intent); + } else if (id == R.id.action_proxy) { + Intent intent = new Intent(LoginActivity.this, ProxyActivity.class); + startActivity(intent); + } else if (id == R.id.action_custom_tabs) { + item.setChecked(!item.isChecked()); + SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(LoginActivity.this); + SharedPreferences.Editor editor = sharedpreferences.edit(); + editor.putBoolean(getString(R.string.SET_EMBEDDED_BROWSER), !item.isChecked()); + editor.apply(); + return false; + } else if (id == R.id.action_provider) { + /* item.setChecked(!item.isChecked()); + SharedPreferences sharedpreferences = getSharedPreferences(Helper.APP_PREFS, MODE_PRIVATE); + SharedPreferences.Editor editor = sharedpreferences.edit(); + editor.putBoolean(getString(R.string.SET_SECURITY_PROVIDER), item.isChecked()); + editor.apply();*/ + return false; + } else if (id == R.id.action_import_data) { + /* if (ContextCompat.checkSelfPermission(LoginActivity.this, Manifest.permission.READ_EXTERNAL_STORAGE) != + PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions(LoginActivity.this, + new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, + TootActivity.MY_PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE); + return true; + }*/ + Intent intent = new Intent(Intent.ACTION_GET_CONTENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setType("*/*"); + String[] mimetypes = {"*/*"}; + intent.putExtra(Intent.EXTRA_MIME_TYPES, mimetypes); + startActivityForResult(intent, PICK_IMPORT); + } + return super.onOptionsItemSelected(item); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, + Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (requestCode == PICK_IMPORT && resultCode == RESULT_OK) { + if (data == null || data.getData() == null) { + Toasty.error(LoginActivity.this, getString(R.string.toot_select_file_error), Toast.LENGTH_LONG).show(); + return; + } + // String filename = Helper.getFilePathFromURI(LoginActivity.this, data.getData()); + // Sqlite.importDB(LoginActivity.this, filename); + } else { + Toasty.error(LoginActivity.this, getString(R.string.toot_select_file_error), Toast.LENGTH_LONG).show(); + } + } + + +} \ No newline at end of file diff --git a/app/src/main/java/app/fedilab/android/activities/MastodonListActivity.java b/app/src/main/java/app/fedilab/android/activities/MastodonListActivity.java new file mode 100644 index 000000000..e92a1c5ec --- /dev/null +++ b/app/src/main/java/app/fedilab/android/activities/MastodonListActivity.java @@ -0,0 +1,323 @@ +package app.fedilab.android.activities; +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + + +import android.content.Intent; +import android.os.Bundle; +import android.text.Editable; +import android.text.InputFilter; +import android.text.TextWatcher; +import android.view.Menu; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentTransaction; +import androidx.lifecycle.ViewModelProvider; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.ArrayList; +import java.util.List; + +import app.fedilab.android.BaseMainActivity; +import app.fedilab.android.R; +import app.fedilab.android.client.entities.Timeline; +import app.fedilab.android.client.mastodon.entities.Account; +import app.fedilab.android.client.mastodon.entities.MastodonList; +import app.fedilab.android.databinding.ActivityListBinding; +import app.fedilab.android.databinding.PopupAddListBinding; +import app.fedilab.android.databinding.PopupManageAccountsListBinding; +import app.fedilab.android.helper.Helper; +import app.fedilab.android.helper.ThemeHelper; +import app.fedilab.android.ui.drawer.AccountListAdapter; +import app.fedilab.android.ui.drawer.MastodonListAdapter; +import app.fedilab.android.ui.fragment.timeline.FragmentMastodonTimeline; +import app.fedilab.android.viewmodel.mastodon.AccountsVM; +import app.fedilab.android.viewmodel.mastodon.TimelinesVM; +import es.dmoral.toasty.Toasty; + + +public class MastodonListActivity extends BaseActivity implements MastodonListAdapter.ActionOnList { + + + AccountListAdapter accountsInListAdapter; + private ActivityListBinding binding; + private boolean canGoBack; + private TimelinesVM timelinesVM; + private MastodonList mastodonList; + private ArrayList mastodonListList; + private MastodonListAdapter mastodonListAdapter; + private AccountsVM accountsVM; + private List accountsInList; + private boolean flagLoading; + private String max_id; + private FragmentMastodonTimeline fragmentMastodonTimeline; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + ThemeHelper.applyThemeBar(this); + binding = ActivityListBinding.inflate(getLayoutInflater()); + setContentView(binding.getRoot()); + canGoBack = false; + if (getSupportActionBar() != null) { + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + getSupportActionBar().setDisplayShowHomeEnabled(true); + } + flagLoading = false; + max_id = null; + accountsVM = new ViewModelProvider(MastodonListActivity.this).get(AccountsVM.class); + timelinesVM = new ViewModelProvider(MastodonListActivity.this).get(TimelinesVM.class); + timelinesVM.getLists(BaseMainActivity.currentInstance, BaseMainActivity.currentToken) + .observe(MastodonListActivity.this, mastodonLists -> { + if (mastodonLists != null && mastodonLists.size() > 0) { + mastodonListList = new ArrayList<>(mastodonLists); + mastodonListAdapter = new MastodonListAdapter(mastodonListList); + mastodonListAdapter.actionOnList = this; + binding.notContent.setVisibility(View.GONE); + binding.recyclerView.setAdapter(mastodonListAdapter); + binding.recyclerView.setLayoutManager(new LinearLayoutManager(MastodonListActivity.this)); + } else { + binding.notContent.setVisibility(View.VISIBLE); + } + }); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == android.R.id.home) { + onBackPressed(); + return true; + } else if (item.getItemId() == R.id.action_manage_users) { + AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(MastodonListActivity.this, Helper.dialogStyle()); + PopupManageAccountsListBinding popupManageAccountsListBinding = PopupManageAccountsListBinding.inflate(getLayoutInflater()); + dialogBuilder.setView(popupManageAccountsListBinding.getRoot()); + popupManageAccountsListBinding.loader.setVisibility(View.VISIBLE); + + popupManageAccountsListBinding.searchAccount.setOnTouchListener((v, event) -> { + final int DRAWABLE_RIGHT = 2; + if (event.getAction() == MotionEvent.ACTION_UP) { + if (popupManageAccountsListBinding.searchAccount.length() > 0 && event.getRawX() >= (popupManageAccountsListBinding.searchAccount.getRight() - popupManageAccountsListBinding.searchAccount.getCompoundDrawables()[DRAWABLE_RIGHT].getBounds().width())) { + popupManageAccountsListBinding.searchAccount.setText(""); + } + } + return false; + }); + timelinesVM.getAccountsInList(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, mastodonList.id, null, null, 10) + .observe(MastodonListActivity.this, accounts -> { + popupManageAccountsListBinding.loader.setVisibility(View.GONE); + accountsInList = accounts; + if (accountsInList == null) { + accountsInList = new ArrayList<>(); + } + if (accountsInList.size() > 0) { + max_id = accountsInList.get(accountsInList.size() - 1).id; + popupManageAccountsListBinding.noContent.setVisibility(View.GONE); + popupManageAccountsListBinding.lvAccountsCurrent.setVisibility(View.VISIBLE); + } else { + popupManageAccountsListBinding.noContent.setVisibility(View.VISIBLE); + popupManageAccountsListBinding.lvAccountsCurrent.setVisibility(View.GONE); + } + accountsInListAdapter = new AccountListAdapter(mastodonList, accountsInList, null); + popupManageAccountsListBinding.lvAccountsCurrent.setAdapter(accountsInListAdapter); + LinearLayoutManager linearLayoutManager = new LinearLayoutManager(MastodonListActivity.this); + popupManageAccountsListBinding.lvAccountsCurrent.setLayoutManager(linearLayoutManager); + popupManageAccountsListBinding.lvAccountsCurrent.addOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { + int firstVisibleItem = linearLayoutManager.findFirstVisibleItemPosition(); + if (dy > 0) { + int visibleItemCount = linearLayoutManager.getChildCount(); + int totalItemCount = linearLayoutManager.getItemCount(); + if (firstVisibleItem + visibleItemCount == totalItemCount) { + if (!flagLoading) { + flagLoading = true; + timelinesVM.getAccountsInList(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, mastodonList.id, max_id, null, 10) + .observe(MastodonListActivity.this, accounts -> { + if (accounts != null && accounts.size() > 0) { + int position = accountsInList.size(); + max_id = accountsInList.get(accounts.size() - 1).id; + accountsInList.addAll(accounts); + accountsInListAdapter.notifyItemRangeChanged(position, accounts.size()); + } + + }); + + } + } + } + + } + }); + }); + + popupManageAccountsListBinding.searchAccount.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + if (count > 0) { + popupManageAccountsListBinding.searchAccount.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.ic_baseline_close_24, 0); + } else { + popupManageAccountsListBinding.searchAccount.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.ic_baseline_search_24, 0); + } + } + + @Override + public void afterTextChanged(Editable s) { + if (s != null && s.length() > 0) { + accountsVM.searchAccounts(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, s.toString(), 20, true, true) + .observe(MastodonListActivity.this, accounts -> { + popupManageAccountsListBinding.lvAccountsSearch.setVisibility(View.VISIBLE); + popupManageAccountsListBinding.lvAccountsCurrent.setVisibility(View.GONE); + AccountListAdapter accountListAdapter = new AccountListAdapter(mastodonList, accountsInList, accounts); + popupManageAccountsListBinding.lvAccountsSearch.setAdapter(accountListAdapter); + popupManageAccountsListBinding.lvAccountsSearch.setLayoutManager(new LinearLayoutManager(MastodonListActivity.this)); + }); + } else { + popupManageAccountsListBinding.lvAccountsSearch.setVisibility(View.GONE); + popupManageAccountsListBinding.lvAccountsCurrent.setVisibility(View.VISIBLE); + } + } + }); + + dialogBuilder.setPositiveButton(R.string.close, (dialog, id) -> dialog.dismiss()); + dialogBuilder.create().show(); + } else if (item.getItemId() == R.id.action_delete && mastodonList != null) { + AlertDialog.Builder alt_bld = new AlertDialog.Builder(MastodonListActivity.this, Helper.dialogStyle()); + alt_bld.setTitle(R.string.action_lists_delete); + alt_bld.setMessage(R.string.action_lists_confirm_delete); + alt_bld.setPositiveButton(R.string.delete, (dialog, id) -> { + timelinesVM.deleteList(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, mastodonList.id); + int position = 0; + for (MastodonList mastodonListTmp : mastodonListList) { + if (mastodonListTmp.id.equalsIgnoreCase(mastodonList.id)) { + break; + } + position++; + } + mastodonListList.remove(position); + mastodonListAdapter.notifyItemRemoved(position); + ThemeHelper.slideViewsToRight(binding.fragmentContainer, binding.recyclerView, () -> { + if (fragmentMastodonTimeline != null) { + fragmentMastodonTimeline.onDestroyView(); + } + }); + if (mastodonListList.size() == 0) { + binding.notContent.setVisibility(View.VISIBLE); + } else { + binding.notContent.setVisibility(View.GONE); + } + Bundle b = new Bundle(); + b.putBoolean(Helper.RECEIVE_REDRAW_TOPBAR, true); + Intent intentBD = new Intent(Helper.BROADCAST_DATA); + b.putSerializable(Helper.RECEIVE_MASTODON_LIST, mastodonListList); + intentBD.putExtras(b); + LocalBroadcastManager.getInstance(MastodonListActivity.this).sendBroadcast(intentBD); + }); + alt_bld.setNegativeButton(R.string.cancel, (dialog, id) -> dialog.dismiss()); + AlertDialog alert = alt_bld.create(); + alert.show(); + } else if (item.getItemId() == R.id.action_add_list) { + AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(MastodonListActivity.this, Helper.dialogStyle()); + PopupAddListBinding popupAddListBinding = PopupAddListBinding.inflate(getLayoutInflater()); + dialogBuilder.setView(popupAddListBinding.getRoot()); + popupAddListBinding.addList.setFilters(new InputFilter[]{new InputFilter.LengthFilter(255)}); + dialogBuilder.setPositiveButton(R.string.validate, (dialog, id) -> { + if (popupAddListBinding.addList.getText() != null && popupAddListBinding.addList.getText().toString().trim().length() > 0) { + timelinesVM.createList(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, popupAddListBinding.addList.getText().toString().trim(), null) + .observe(MastodonListActivity.this, newMastodonList -> { + if (newMastodonList != null) { + mastodonListList.add(0, newMastodonList); + mastodonListAdapter.notifyItemInserted(0); + } else { + Toasty.error(MastodonListActivity.this, getString(R.string.toast_error), Toasty.LENGTH_LONG).show(); + } + Bundle b = new Bundle(); + b.putBoolean(Helper.RECEIVE_REDRAW_TOPBAR, true); + Intent intentBD = new Intent(Helper.BROADCAST_DATA); + b.putSerializable(Helper.RECEIVE_MASTODON_LIST, mastodonListList); + intentBD.putExtras(b); + LocalBroadcastManager.getInstance(MastodonListActivity.this).sendBroadcast(intentBD); + }); + dialog.dismiss(); + } else { + popupAddListBinding.addList.setError(getString(R.string.not_valid_list_name)); + } + + }); + dialogBuilder.setNegativeButton(R.string.cancel, (dialog, id) -> dialog.dismiss()); + dialogBuilder.create().show(); + } + return super.onOptionsItemSelected(item); + } + + @Override + public void click(MastodonList mastodonList) { + + this.mastodonList = mastodonList; + canGoBack = true; + ThemeHelper.slideViewsToLeft(binding.recyclerView, binding.fragmentContainer, () -> { + fragmentMastodonTimeline = new FragmentMastodonTimeline(); + Bundle bundle = new Bundle(); + bundle.putSerializable(Helper.ARG_LIST_ID, mastodonList.id); + bundle.putSerializable(Helper.ARG_TIMELINE_TYPE, Timeline.TimeLineEnum.LIST); + setTitle(mastodonList.title); + fragmentMastodonTimeline.setArguments(bundle); + FragmentManager fragmentManager = getSupportFragmentManager(); + FragmentTransaction fragmentTransaction = + fragmentManager.beginTransaction(); + fragmentTransaction.replace(R.id.fragment_container, fragmentMastodonTimeline); + fragmentTransaction.commit(); + }); + invalidateOptionsMenu(); + } + + @Override + public boolean onCreateOptionsMenu(@NonNull Menu menu) { + if (binding != null) { + if (binding.recyclerView.getVisibility() == View.VISIBLE) { + getMenuInflater().inflate(R.menu.menu_main_list, menu); + } else { + getMenuInflater().inflate(R.menu.menu_list, menu); + } + } + return true; + } + + @Override + public void onBackPressed() { + if (canGoBack) { + canGoBack = false; + ThemeHelper.slideViewsToRight(binding.fragmentContainer, binding.recyclerView, () -> { + if (fragmentMastodonTimeline != null) { + fragmentMastodonTimeline.onDestroyView(); + } + }); + setTitle(R.string.action_lists); + invalidateOptionsMenu(); + } else { + super.onBackPressed(); + } + } +} diff --git a/app/src/main/java/app/fedilab/android/activities/MediaActivity.java b/app/src/main/java/app/fedilab/android/activities/MediaActivity.java new file mode 100644 index 000000000..176dd1a7c --- /dev/null +++ b/app/src/main/java/app/fedilab/android/activities/MediaActivity.java @@ -0,0 +1,437 @@ +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ +package app.fedilab.android.activities; + + +import android.Manifest; +import android.app.DownloadManager; +import android.content.BroadcastReceiver; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.graphics.Point; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.view.Display; +import android.view.Menu; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; + +import androidx.annotation.NonNull; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentStatePagerAdapter; +import androidx.preference.PreferenceManager; +import androidx.viewpager.widget.PagerAdapter; +import androidx.viewpager.widget.ViewPager; + +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; + +import app.fedilab.android.R; +import app.fedilab.android.client.mastodon.entities.Attachment; +import app.fedilab.android.databinding.ActivityMediaPagerBinding; +import app.fedilab.android.helper.Helper; +import app.fedilab.android.helper.MediaHelper; +import app.fedilab.android.helper.ThemeHelper; +import app.fedilab.android.interfaces.OnDownloadInterface; +import app.fedilab.android.ui.fragment.media.FragmentMedia; +import es.dmoral.toasty.Toasty; + + +public class MediaActivity extends BaseActivity implements OnDownloadInterface { + + int flags; + private ArrayList attachments; + private int mediaPosition; + private long downloadID; + + private final BroadcastReceiver onDownloadComplete = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + long id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1); + if (downloadID == id) { + DownloadManager manager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); + assert manager != null; + Uri uri = manager.getUriForDownloadedFile(downloadID); + Intent shareIntent = new Intent(Intent.ACTION_SEND); + shareIntent.putExtra(Intent.EXTRA_STREAM, uri); + ContentResolver cR = context.getContentResolver(); + if (cR != null && uri != null) { + shareIntent.setType(cR.getType(uri)); + try { + startActivity(shareIntent); + } catch (Exception ignored) { + } + } else { + Toasty.error(context, context.getString(R.string.toast_error), Toasty.LENGTH_LONG).show(); + } + } else { + Toasty.success(context, context.getString(R.string.save_over), Toasty.LENGTH_LONG).show(); + } + } + }; + private boolean fullscreen; + private Handler handler; + private int minTouch, maxTouch; + private float startX; + private float startY; + private FragmentMedia mCurrentFragment; + private ActivityMediaPagerBinding binding; + + @Override + protected void onCreate(Bundle savedInstanceState) { + getWindow().requestFeature(Window.FEATURE_ACTION_BAR_OVERLAY); + ThemeHelper.applyThemeBar(this); + super.onCreate(savedInstanceState); + ActivityCompat.postponeEnterTransition(MediaActivity.this); + binding = ActivityMediaPagerBinding.inflate(getLayoutInflater()); + setContentView(binding.getRoot()); + + + fullscreen = false; + SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(MediaActivity.this); + final int med_desc_timeout = sharedpreferences.getInt(getString(R.string.SET_MED_DESC_TIMEOUT), 3) * 1000; + flags = getWindow().getDecorView().getSystemUiVisibility(); + Bundle b = getIntent().getExtras(); + if (b != null) { + mediaPosition = b.getInt(Helper.ARG_MEDIA_POSITION, 1); + attachments = (ArrayList) b.getSerializable(Helper.ARG_MEDIA_ARRAY); + } + if (getSupportActionBar() != null) { + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + getSupportActionBar().setDisplayShowHomeEnabled(true); + } + + if (attachments == null || attachments.size() == 0) + finish(); + + setTitle(""); + + PagerAdapter mPagerAdapter = new ScreenSlidePagerAdapter(getSupportFragmentManager()); + binding.mediaViewpager.setAdapter(mPagerAdapter); + + binding.mediaViewpager.setCurrentItem(mediaPosition - 1); + binding.mediaViewpager.setOffscreenPageLimit(0); + binding.haulerView.setOnDragDismissedListener(dragDirection -> ActivityCompat.finishAfterTransition(MediaActivity.this)); + registerReceiver(onDownloadComplete, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)); + String description = attachments.get(mediaPosition - 1).description; + handler = new Handler(); + if (description != null && description.trim().length() > 0 && description.trim().compareTo("null") != 0) { + binding.mediaDescription.setText(description); + binding.mediaDescription.setVisibility(View.VISIBLE); + + handler.postDelayed(() -> { + if (binding != null && !binding.mediaDescription.hasSelection()) { + binding.mediaDescription.setVisibility(View.GONE); + } + }, med_desc_timeout); + + } else { + if (!binding.mediaDescription.hasSelection()) { + binding.mediaDescription.setVisibility(View.GONE); + } + } + binding.mediaViewpager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() { + public void onPageScrollStateChanged(int state) { + } + + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + } + + public void onPageSelected(int position) { + String description = attachments.get(position).description; + if (handler != null) { + handler.removeCallbacksAndMessages(null); + } + handler = new Handler(); + if (description != null && description.trim().length() > 0 && description.trim().compareTo("null") != 0) { + binding.mediaDescription.setText(description); + binding.mediaDescription.setVisibility(View.VISIBLE); + + handler.postDelayed(() -> { + if (binding != null && !binding.mediaDescription.hasSelection()) { + binding.mediaDescription.setVisibility(View.GONE); + } + }, med_desc_timeout); + + } else { + if (!binding.mediaDescription.hasSelection()) { + binding.mediaDescription.setVisibility(View.GONE); + } + } + } + }); + + + setFullscreen(true); + Display display = getWindowManager().getDefaultDisplay(); + Point size = new Point(); + display.getSize(size); + int screenHeight = size.y; + minTouch = (int) (screenHeight * 0.1); + maxTouch = (int) (screenHeight * 0.9); + + } + + + @Override + public boolean onCreateOptionsMenu(@NonNull Menu menu) { + getMenuInflater().inflate(R.menu.menu_media, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == android.R.id.home) { + finish(); + ActivityCompat.finishAfterTransition(MediaActivity.this); + return true; + } else if (item.getItemId() == R.id.action_save) { + int position = binding.mediaViewpager.getCurrentItem(); + Attachment attachment = attachments.get(position); + if (Build.VERSION.SDK_INT >= 23) { + if (ContextCompat.checkSelfPermission(MediaActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED || ContextCompat.checkSelfPermission(MediaActivity.this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions(MediaActivity.this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, Helper.EXTERNAL_STORAGE_REQUEST_CODE_MEDIA_SAVE); + } else { + if (attachment.type.compareTo("image") == 0) { + MediaHelper.manageMove(MediaActivity.this, attachment.url, false); + } else { + MediaHelper.manageDownloadsNoPopup(MediaActivity.this, attachment.url); + downloadID = -1; + } + } + } else { + if (attachment.type.compareToIgnoreCase("image") == 0) { + MediaHelper.manageMove(MediaActivity.this, attachment.url, false); + } else { + MediaHelper.manageDownloadsNoPopup(MediaActivity.this, attachment.url); + downloadID = -1; + } + } + } else if (item.getItemId() == R.id.action_share) { + int position = binding.mediaViewpager.getCurrentItem(); + Attachment attachment = attachments.get(position); + if (attachment.type.compareTo("image") == 0) { + MediaHelper.manageMove(MediaActivity.this, attachment.url, true); + } else if (attachment.type.equalsIgnoreCase("video") || attachment.type.equalsIgnoreCase("audio") || attachment.type.equalsIgnoreCase("gifv")) { + downloadID = MediaHelper.manageDownloadsNoPopup(MediaActivity.this, attachment.url); + } else { + if (Build.VERSION.SDK_INT >= 23) { + if (ContextCompat.checkSelfPermission(MediaActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED || ContextCompat.checkSelfPermission(MediaActivity.this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions(MediaActivity.this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, Helper.EXTERNAL_STORAGE_REQUEST_CODE_MEDIA_SHARE); + } else { + downloadID = MediaHelper.manageDownloadsNoPopup(MediaActivity.this, attachment.url); + } + } else { + downloadID = MediaHelper.manageDownloadsNoPopup(MediaActivity.this, attachment.url); + } + } + } + return true; + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + if (requestCode == Helper.EXTERNAL_STORAGE_REQUEST_CODE_MEDIA_SAVE) { + if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + int position = binding.mediaViewpager.getCurrentItem(); + Attachment attachment = attachments.get(position); + if (attachment.type.compareToIgnoreCase("image") == 0) { + MediaHelper.manageMove(MediaActivity.this, attachment.url, false); + } else { + MediaHelper.manageDownloadsNoPopup(MediaActivity.this, attachment.url); + downloadID = -1; + } + } else { /*Todo: Toast "Storage Permission Required" */ } + } else if (requestCode == Helper.EXTERNAL_STORAGE_REQUEST_CODE_MEDIA_SHARE) { + if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + int position = binding.mediaViewpager.getCurrentItem(); + Attachment attachment = attachments.get(position); + downloadID = MediaHelper.manageDownloadsNoPopup(MediaActivity.this, attachment.url); + } else { /*Todo: Toast "Storage Permission Required" */ } + } + } + + @Override + public boolean dispatchTouchEvent(MotionEvent event) { + + SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(MediaActivity.this); + final int med_desc_timeout = sharedpreferences.getInt(getString(R.string.SET_MED_DESC_TIMEOUT), 3) * 1000; + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + startX = event.getX(); + startY = event.getY(); + break; + case MotionEvent.ACTION_UP: + float endX = event.getX(); + float endY = event.getY(); + if (endY > minTouch && endY < maxTouch && isAClick(startX, endX, startY, endY)) { + setFullscreen(!fullscreen); + if (!fullscreen) { + String description = attachments.get(binding.mediaViewpager.getCurrentItem()).description; + if (handler != null) { + handler.removeCallbacksAndMessages(null); + } + handler = new Handler(); + if (description != null && description.trim().length() > 0 && description.trim().compareTo("null") != 0) { + binding.mediaDescription.setText(description); + binding.mediaDescription.setVisibility(View.VISIBLE); + + handler.postDelayed(() -> { + if (binding != null && !binding.mediaDescription.hasSelection()) { + binding.mediaDescription.setVisibility(View.GONE); + } + }, med_desc_timeout); + + } else { + if (!binding.mediaDescription.hasSelection()) { + binding.mediaDescription.setVisibility(View.GONE); + } + } + } + } + break; + } + try { + return super.dispatchTouchEvent(event); + } catch (IllegalArgumentException ex) { + ex.printStackTrace(); + } + return false; + + } + + + private boolean isAClick(float startX, float endX, float startY, float endY) { + float differenceX = Math.abs(startX - endX); + float differenceY = Math.abs(startY - endY); + int CLICK_ACTION_THRESHOLD = 200; + return !(differenceX > CLICK_ACTION_THRESHOLD/* =5 */ || differenceY > CLICK_ACTION_THRESHOLD); + } + + @Override + public void onDestroy() { + binding = null; + unregisterReceiver(onDownloadComplete); + super.onDestroy(); + } + + public FragmentMedia getCurrentFragment() { + return mCurrentFragment; + } + + @Override + public void onDownloaded(String saveFilePath, String downloadUrl, Error error) { + + } + + @Override + public void onUpdateProgress(int progress) { + + } + + @Override + protected void onPostResume() { + super.onPostResume(); + } + + + public boolean getFullScreen() { + return this.fullscreen; + } + + public void setFullscreen(boolean fullscreen) { + this.fullscreen = fullscreen; + if (!fullscreen) { + showSystemUI(); + + } else { + hideSystemUI(); + } + } + + private void hideSystemUI() { + // Enables regular immersive mode. + // For "lean back" mode, remove SYSTEM_UI_FLAG_IMMERSIVE. + // Or for "sticky immersive," replace it with SYSTEM_UI_FLAG_IMMERSIVE_STICKY + View decorView = getWindow().getDecorView(); + decorView.setSystemUiVisibility( + View.SYSTEM_UI_FLAG_IMMERSIVE + // Set the content to appear under the system bars so that the + // content doesn't resize when the system bars hide and show. + | View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + // Hide the nav bar and status bar + | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_FULLSCREEN); + } + + // Shows the system bars by removing all the flags +// except for the ones that make the content appear under the system bars. + private void showSystemUI() { + View decorView = getWindow().getDecorView(); + decorView.setSystemUiVisibility( + View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); + } + + /** + * Media Pager + */ + private class ScreenSlidePagerAdapter extends FragmentStatePagerAdapter { + + ScreenSlidePagerAdapter(FragmentManager fm) { + super(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT); + } + + @NotNull + @Override + public Fragment getItem(int position) { + Bundle bundle = new Bundle(); + FragmentMedia mediaSliderFragment = new FragmentMedia(); + bundle.putInt(Helper.ARG_MEDIA_POSITION, position); + bundle.putSerializable(Helper.ARG_MEDIA_ATTACHMENT, attachments.get(position)); + mediaSliderFragment.setArguments(bundle); + return mediaSliderFragment; + } + + @Override + public void setPrimaryItem(@NotNull ViewGroup container, int position, @NotNull Object object) { + if (getCurrentFragment() != object) { + mCurrentFragment = ((FragmentMedia) object); + } + super.setPrimaryItem(container, position, object); + } + + @Override + public int getCount() { + return attachments.size(); + } + } +} diff --git a/app/src/main/java/app/fedilab/android/activities/ProfileActivity.java b/app/src/main/java/app/fedilab/android/activities/ProfileActivity.java new file mode 100644 index 000000000..cf98a9995 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/activities/ProfileActivity.java @@ -0,0 +1,1032 @@ +package app.fedilab.android.activities; +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + + +import android.content.BroadcastReceiver; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.content.res.ColorStateList; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.text.Html; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.Spanned; +import android.text.method.LinkMovementMethod; +import android.text.style.ForegroundColorSpan; +import android.text.style.UnderlineSpan; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.inputmethod.EditorInfo; +import android.widget.EditText; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AlertDialog; +import androidx.core.app.ActivityOptionsCompat; +import androidx.core.content.ContextCompat; +import androidx.lifecycle.ViewModelProvider; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; +import androidx.preference.PreferenceManager; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.request.target.CustomTarget; +import com.bumptech.glide.request.transition.Transition; +import com.google.android.material.tabs.TabLayout; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import app.fedilab.android.BaseMainActivity; +import app.fedilab.android.R; +import app.fedilab.android.client.entities.Pinned; +import app.fedilab.android.client.entities.Timeline; +import app.fedilab.android.client.entities.WellKnownNodeinfo; +import app.fedilab.android.client.entities.app.PinnedTimeline; +import app.fedilab.android.client.entities.app.RemoteInstance; +import app.fedilab.android.client.mastodon.entities.Account; +import app.fedilab.android.client.mastodon.entities.Attachment; +import app.fedilab.android.client.mastodon.entities.Field; +import app.fedilab.android.client.mastodon.entities.IdentityProof; +import app.fedilab.android.client.mastodon.entities.MastodonList; +import app.fedilab.android.client.mastodon.entities.RelationShip; +import app.fedilab.android.databinding.ActivityProfileBinding; +import app.fedilab.android.exception.DBException; +import app.fedilab.android.helper.CrossActionHelper; +import app.fedilab.android.helper.Helper; +import app.fedilab.android.helper.MastodonHelper; +import app.fedilab.android.helper.SpannableHelper; +import app.fedilab.android.helper.ThemeHelper; +import app.fedilab.android.ui.drawer.IdentityProofsAdapter; +import app.fedilab.android.ui.pageadapter.FedilabProfileTLPageAdapter; +import app.fedilab.android.viewmodel.mastodon.AccountsVM; +import app.fedilab.android.viewmodel.mastodon.NodeInfoVM; +import app.fedilab.android.viewmodel.mastodon.ReorderVM; +import app.fedilab.android.viewmodel.mastodon.TimelinesVM; +import es.dmoral.toasty.Toasty; + + +public class ProfileActivity extends BaseActivity { + + + private RelationShip relationship; + private Account account; + private ScheduledExecutorService scheduledExecutorService; + private action doAction; + private AccountsVM accountsVM; + private RecyclerView identityProofsRecycler; + private List identityProofList; + private ActivityProfileBinding binding; + private String account_id; + private String mention_str; + private WellKnownNodeinfo.NodeInfo nodeInfo; + + private final BroadcastReceiver broadcast_data = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + Bundle b = intent.getExtras(); + if (b != null) { + Account accountReceived = (Account) b.getSerializable(Helper.ARG_ACCOUNT); + if (b.getBoolean(Helper.RECEIVE_REDRAW_PROFILE, false) && accountReceived != null) { + if (account != null && accountReceived.id.equalsIgnoreCase(account.id)) { + initializeView(accountReceived); + } + } + } + } + }; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + ThemeHelper.applyTheme(this); + binding = ActivityProfileBinding.inflate(getLayoutInflater()); + setContentView(binding.getRoot()); + setSupportActionBar(binding.toolbar); + ActionBar actionBar = getSupportActionBar(); + Bundle b = getIntent().getExtras(); + binding.accountFollow.setEnabled(false); + if (b != null) { + account = (Account) b.getSerializable(Helper.ARG_ACCOUNT); + account_id = b.getString(Helper.ARG_USER_ID, null); + mention_str = b.getString(Helper.ARG_MENTION, null); + } + + + postponeEnterTransition(); + + //Remove title + if (actionBar != null) { + actionBar.setDisplayShowTitleEnabled(false); + } + + if (getSupportActionBar() != null) { + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + getSupportActionBar().setDisplayShowHomeEnabled(true); + } + binding.toolbar.setPopupTheme(Helper.popupStyle()); + accountsVM = new ViewModelProvider(ProfileActivity.this).get(AccountsVM.class); + if (account != null) { + new Thread(() -> { + account = SpannableHelper.convertAccount(ProfileActivity.this, account); + Handler mainHandler = new Handler(Looper.getMainLooper()); + Runnable myRunnable = () -> initializeView(account); + mainHandler.post(myRunnable); + + }).start(); + + } else if (account_id != null) { + accountsVM.getAccount(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, account_id).observe(ProfileActivity.this, fetchedAccount -> { + account = fetchedAccount; + initializeView(account); + }); + } else if (mention_str != null) { + accountsVM.searchAccounts(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, mention_str, 1, true, false).observe(ProfileActivity.this, accounts -> { + if (accounts != null && accounts.size() > 0) { + account = accounts.get(0); + initializeView(account); + } else { + Toasty.error(ProfileActivity.this, getString(R.string.toast_error_loading_account), Toast.LENGTH_LONG).show(); + finish(); + } + }); + } else { + Toasty.error(ProfileActivity.this, getString(R.string.toast_error_loading_account), Toast.LENGTH_LONG).show(); + finish(); + } + LocalBroadcastManager.getInstance(ProfileActivity.this).registerReceiver(broadcast_data, new IntentFilter(Helper.BROADCAST_DATA)); + } + + private void initializeView(Account account) { + SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(ProfileActivity.this); + if (account == null) { + Toasty.error(ProfileActivity.this, getString(R.string.toast_error_loading_account), Toast.LENGTH_LONG).show(); + finish(); + return; + } + binding.title.setText(String.format(Locale.getDefault(), "@%s", account.acct)); + binding.headerEditProfile.setOnClickListener(v -> { + Intent intent = new Intent(ProfileActivity.this, EditProfileActivity.class); + startActivity(intent); + }); + + // MastodonHelper.loadPPMastodon(binding.profilePicture, account); + binding.appBar.addOnOffsetChangedListener((appBarLayout, verticalOffset) -> { + + if (Math.abs(verticalOffset) - binding.appBar.getTotalScrollRange() == 0) { + binding.profilePicture.setVisibility(View.VISIBLE); + binding.title.setVisibility(View.VISIBLE); + } else { + binding.profilePicture.setVisibility(View.GONE); + binding.title.setVisibility(View.GONE); + } + }); + + + //Retrieve relationship with the connected account + List accountListToCheck = new ArrayList<>(); + accountListToCheck.add(account.id); + //Retrieve relation ship + accountsVM.getRelationships(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, accountListToCheck).observe(ProfileActivity.this, relationShips -> { + if (relationShips != null && relationShips.size() > 0) { + this.relationship = relationShips.get(0); + updateAccount(); + } + }); + //Retrieve identity proofs + accountsVM.getIdentityProofs(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, account.id).observe(ProfileActivity.this, identityProofs -> { + this.identityProofList = identityProofs; + updateAccount(); + }); + //Animate emojis + if (account.emojis != null && account.emojis.size() > 0) { + boolean disableAnimatedEmoji = sharedpreferences.getBoolean(getString(R.string.SET_DISABLE_ANIMATED_EMOJI), false); + if (!disableAnimatedEmoji) { + scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(); + scheduledExecutorService.scheduleAtFixedRate(() -> binding.accountDn.invalidate(), 0, 130, TimeUnit.MILLISECONDS); + } + } + binding.accountTabLayout.clearOnTabSelectedListeners(); + binding.accountTabLayout.removeAllTabs(); + binding.accountViewpager.clearOnPageChangeListeners(); + //Tablayout for timelines/following/followers + FedilabProfileTLPageAdapter fedilabProfileTLPageAdapter = new FedilabProfileTLPageAdapter(getSupportFragmentManager(), account); + binding.accountTabLayout.addTab(binding.accountTabLayout.newTab().setText(getString(R.string.status_cnt, Helper.withSuffix(account.statuses_count)))); + binding.accountTabLayout.addTab(binding.accountTabLayout.newTab().setText(getString(R.string.following_cnt, Helper.withSuffix(account.following_count)))); + binding.accountTabLayout.addTab(binding.accountTabLayout.newTab().setText(getString(R.string.followers_cnt, Helper.withSuffix(account.followers_count)))); + binding.accountViewpager.setAdapter(fedilabProfileTLPageAdapter); + binding.accountViewpager.setOffscreenPageLimit(3); + binding.accountViewpager.addOnPageChangeListener(new TabLayout.TabLayoutOnPageChangeListener(binding.accountTabLayout)); + binding.accountTabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() { + @Override + public void onTabSelected(TabLayout.Tab tab) { + binding.accountViewpager.setCurrentItem(tab.getPosition()); + } + + @Override + public void onTabUnselected(TabLayout.Tab tab) { + + } + + @Override + public void onTabReselected(TabLayout.Tab tab) { + + } + }); + binding.accountTabLayout.setTabTextColors(ThemeHelper.getAttColor(ProfileActivity.this, R.attr.mTextColor), ContextCompat.getColor(ProfileActivity.this, R.color.cyanea_accent_dark_reference)); + binding.accountTabLayout.setTabIconTint(ThemeHelper.getColorStateList(ProfileActivity.this)); + boolean disableGif = sharedpreferences.getBoolean(getString(R.string.SET_DISABLE_GIF), false); + String targetedUrl = disableGif ? account.avatar_static : account.avatar; + Glide.with(ProfileActivity.this) + .asDrawable() + .dontTransform() + .load(targetedUrl).into( + new CustomTarget() { + @Override + public void onResourceReady(@NonNull final Drawable resource, Transition transition) { + binding.profilePicture.setImageDrawable(resource); + startPostponedEnterTransition(); + } + + @Override + public void onLoadFailed(@Nullable Drawable errorDrawable) { + binding.profilePicture.setImageResource(R.drawable.ic_person); + startPostponedEnterTransition(); + } + + @Override + public void onLoadCleared(@Nullable Drawable placeholder) { + + } + } + ); + //Load header + MastodonHelper.loadProfileMediaMastodon(binding.bannerPp, account, MastodonHelper.MediaAccountType.HEADER); + //Redraws icon for locked accounts + final float scale = getResources().getDisplayMetrics().density; + if (account.locked) { + Drawable img = ContextCompat.getDrawable(ProfileActivity.this, R.drawable.ic_baseline_lock_24); + assert img != null; + img.setBounds(0, 0, (int) (16 * scale + 0.5f), (int) (16 * scale + 0.5f)); + binding.accountUn.setCompoundDrawables(null, null, img, null); + } else { + binding.accountUn.setCompoundDrawables(null, null, null, null); + } + //Peertube account watched by a Mastodon account + //Bot account + if (account.bot) { + binding.accountBot.setVisibility(View.VISIBLE); + } + if (account.acct != null) { + setTitle(account.acct); + } + + + final SpannableString content = new SpannableString(getString(R.string.disclaimer_full)); + content.setSpan(new UnderlineSpan(), 0, content.length(), 0); + content.setSpan(new ForegroundColorSpan(ContextCompat.getColor(ProfileActivity.this, R.color.cyanea_accent_reference)), 0, content.length(), + Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + binding.warningMessage.setText(content); + binding.warningMessage.setOnClickListener(view -> { + if (!account.url.toLowerCase().startsWith("http://") && !account.url.toLowerCase().startsWith("https://")) + account.url = "http://" + account.url; + Helper.openBrowser(ProfileActivity.this, account.url); + }); + //Timed muted account + if (account.mute_expires_at != null && account.mute_expires_at.after(new Date())) { + binding.tempMute.setVisibility(View.VISIBLE); + SpannableString content_temp_mute = new SpannableString(getString(R.string.timed_mute_profile, account.acct, account.mute_expires_at)); + content_temp_mute.setSpan(new UnderlineSpan(), 0, content_temp_mute.length(), 0); + binding.tempMute.setText(content_temp_mute); + } + //This account was moved to another one + if (account.moved != null) { + binding.accountMoved.setVisibility(View.VISIBLE); + Drawable imgTravel = ContextCompat.getDrawable(ProfileActivity.this, R.drawable.ic_baseline_card_travel_24); + assert imgTravel != null; + imgTravel.setBounds(0, 0, (int) (20 * scale + 0.5f), (int) (20 * scale + 0.5f)); + binding.accountMoved.setCompoundDrawables(imgTravel, null, null, null); + //Retrieves content and make account names clickable + SpannableString spannableString = SpannableHelper.moveToText(ProfileActivity.this, account); + binding.accountMoved.setText(spannableString, TextView.BufferType.SPANNABLE); + binding.accountMoved.setMovementMethod(LinkMovementMethod.getInstance()); + } + if (account.acct.contains("@")) + binding.warningMessage.setVisibility(View.VISIBLE); + else + binding.warningMessage.setVisibility(View.GONE); + + + //Fields for profile + List fields = account.fields; + if (fields != null && fields.size() > 0) { + for (int i = 0; i < fields.size(); i++) { + LinearLayout field; + TextView labelView; + TextView valueView; + switch (i) { + case 1: + field = binding.field1; + labelView = binding.label1; + valueView = binding.value1; + break; + case 2: + field = binding.field2; + labelView = binding.label2; + valueView = binding.value2; + break; + case 3: + field = binding.field3; + labelView = binding.label3; + valueView = binding.value3; + break; + default: + field = binding.field4; + labelView = binding.label4; + valueView = binding.value4; + break; + } + + field.setVisibility(View.VISIBLE); + if (fields.get(i).verified_at != null) { + valueView.setCompoundDrawablesWithIntrinsicBounds(null, null, ContextCompat.getDrawable(ProfileActivity.this, R.drawable.ic_baseline_verified_24), null); + fields.get(i).value_span.setSpan(new ForegroundColorSpan(ContextCompat.getColor(ProfileActivity.this, R.color.verified_text)), 0, fields.get(i).value_span.toString().length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + valueView.setText(fields.get(i).value_span != null ? fields.get(i).value_span : fields.get(i).value, TextView.BufferType.SPANNABLE); + valueView.setMovementMethod(LinkMovementMethod.getInstance()); + labelView.setText(fields.get(i).name); + } + binding.fieldsContainer.setVisibility(View.VISIBLE); + } + binding.accountDn.setText(account.span_display_name != null ? account.span_display_name : account.display_name, TextView.BufferType.SPANNABLE); + binding.accountUn.setText(String.format("@%s", account.acct)); + binding.accountUn.setOnLongClickListener(v -> { + ClipboardManager clipboard = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE); + String account_id = account.acct; + if (account_id.split("@").length == 1) + account_id += "@" + BaseMainActivity.currentInstance; + ClipData clip = ClipData.newPlainText("mastodon_account_id", "@" + account_id); + Toasty.info(ProfileActivity.this, getString(R.string.account_id_clipbloard), Toast.LENGTH_SHORT).show(); + assert clipboard != null; + clipboard.setPrimaryClip(clip); + return false; + }); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + binding.accountNote.setText(account.span_note != null ? account.span_note : new SpannableString(Html.fromHtml(account.note, Html.FROM_HTML_MODE_COMPACT)), TextView.BufferType.SPANNABLE); + else + binding.accountNote.setText(account.span_note != null ? account.span_note : new SpannableString(Html.fromHtml(account.note)), TextView.BufferType.SPANNABLE); + + binding.accountNote.setMovementMethod(LinkMovementMethod.getInstance()); + + MastodonHelper.loadPPMastodon(binding.accountPp, account); + binding.accountPp.setOnClickListener(v -> { + Intent intent = new Intent(ProfileActivity.this, MediaActivity.class); + Bundle b = new Bundle(); + Attachment attachment = new Attachment(); + attachment.description = account.acct; + attachment.preview_url = account.avatar; + attachment.url = account.avatar; + attachment.remote_url = account.avatar; + attachment.type = "image"; + ArrayList attachments = new ArrayList<>(); + attachments.add(attachment); + b.putSerializable(Helper.ARG_MEDIA_ARRAY, attachments); + b.putInt(Helper.ARG_MEDIA_POSITION, 1); + intent.putExtras(b); + ActivityOptionsCompat options = ActivityOptionsCompat + .makeSceneTransitionAnimation(ProfileActivity.this, binding.accountPp, attachment.url); + // start the new activity + startActivity(intent, options.toBundle()); + }); + + + binding.accountNotification.setOnClickListener(v -> { + if (relationship != null && relationship.followed_by) { + relationship.notifying = !relationship.notifying; + accountsVM.follow(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, account.id, relationship.showing_reblogs, relationship.notifying) + .observe(ProfileActivity.this, relationShip -> { + this.relationship = relationShip; + updateAccount(); + }); + } + }); + + binding.accountFollow.setOnClickListener(v -> { + if (doAction == action.NOTHING) { + Toasty.info(ProfileActivity.this, getString(R.string.nothing_to_do), Toast.LENGTH_LONG).show(); + } else if (doAction == action.FOLLOW) { + binding.accountFollow.setEnabled(false); + accountsVM.follow(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, account.id, true, false) + .observe(ProfileActivity.this, relationShip -> { + this.relationship = relationShip; + updateAccount(); + }); + } else if (doAction == action.UNFOLLOW) { + boolean confirm_unfollow = sharedpreferences.getBoolean(getString(R.string.SET_UNFOLLOW_VALIDATION), true); + if (confirm_unfollow) { + AlertDialog.Builder unfollowConfirm = new AlertDialog.Builder(ProfileActivity.this, Helper.dialogStyle()); + unfollowConfirm.setTitle(getString(R.string.unfollow_confirm)); + unfollowConfirm.setMessage(account.acct); + unfollowConfirm.setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss()); + unfollowConfirm.setPositiveButton(R.string.yes, (dialog, which) -> { + binding.accountFollow.setEnabled(false); + accountsVM.unfollow(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, account.id) + .observe(ProfileActivity.this, relationShip -> { + this.relationship = relationShip; + updateAccount(); + }); + dialog.dismiss(); + }); + unfollowConfirm.show(); + } else { + binding.accountFollow.setEnabled(false); + binding.accountFollow.setEnabled(false); + accountsVM.unfollow(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, account.id) + .observe(ProfileActivity.this, relationShip -> { + this.relationship = relationShip; + updateAccount(); + }); + } + + } else if (doAction == action.UNBLOCK) { + binding.accountFollow.setEnabled(false); + accountsVM.unblock(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, account.id) + .observe(ProfileActivity.this, relationShip -> { + this.relationship = relationShip; + updateAccount(); + }); + } + }); + binding.accountNotification.setOnLongClickListener(v -> { + CrossActionHelper.doCrossAction(ProfileActivity.this, CrossActionHelper.TypeOfCrossAction.FOLLOW_ACTION, account, null); + return false; + }); + + binding.accountDate.setText(Helper.shortDateToString(account.created_at)); + binding.accountDate.setVisibility(View.VISIBLE); + + String[] accountInstanceArray = account.acct.split("@"); + String accountInstance = BaseMainActivity.currentInstance; + if (accountInstanceArray.length > 1) { + accountInstance = accountInstanceArray[1]; + } + + NodeInfoVM nodeInfoVM = new ViewModelProvider(ProfileActivity.this).get(NodeInfoVM.class); + String finalAccountInstance = accountInstance; + nodeInfoVM.getNodeInfo(accountInstance).observe(ProfileActivity.this, nodeInfo -> { + this.nodeInfo = nodeInfo; + if (nodeInfo != null && nodeInfo.software != null) { + binding.instanceInfo.setText(nodeInfo.software.name); + binding.instanceInfo.setVisibility(View.VISIBLE); + + binding.instanceInfo.setOnClickListener(v -> { + Intent intent = new Intent(ProfileActivity.this, InstanceProfileActivity.class); + Bundle b = new Bundle(); + b.putString(Helper.ARG_INSTANCE, finalAccountInstance); + intent.putExtras(b); + startActivity(intent); + + }); + } + }); + + } + + + /*** + * This methode is called to update the view once an action has been performed + */ + private void updateAccount() { + + //The value for account is from same server so id can be used + if (account.id.equals(BaseMainActivity.accountWeakReference.get().user_id)) { + binding.accountFollow.setVisibility(View.GONE); + binding.headerEditProfile.setVisibility(View.VISIBLE); + binding.headerEditProfile.bringToFront(); + } + //Manage indentity proofs if not yet displayed + + if (identityProofList != null && identityProofList.size() > 0) { + ImageView identity_proofs_indicator = findViewById(R.id.identity_proofs_indicator); + identity_proofs_indicator.setVisibility(View.VISIBLE); + //Recyclerview for identity proof has not been inflated yet + if (identityProofsRecycler == null) { + identity_proofs_indicator.setOnClickListener(v -> { + AlertDialog.Builder builder = new AlertDialog.Builder(ProfileActivity.this, Helper.dialogStyle()); + identityProofsRecycler = new RecyclerView(ProfileActivity.this); + LinearLayoutManager mLayoutManager = new LinearLayoutManager(ProfileActivity.this); + identityProofsRecycler.setLayoutManager(mLayoutManager); + IdentityProofsAdapter identityProofsAdapter = new IdentityProofsAdapter(identityProofList); + identityProofsRecycler.setAdapter(identityProofsAdapter); + builder.setView(identityProofsRecycler); + builder + .setTitle(R.string.identity_proofs) + .setPositiveButton(R.string.close, (dialog, which) -> dialog.dismiss()) + .show(); + }); + } + } + int[][] states = new int[][]{ + new int[]{android.R.attr.state_enabled}, // enabled + new int[]{-android.R.attr.state_enabled}, // disabled + new int[]{-android.R.attr.state_checked}, // unchecked + new int[]{android.R.attr.state_pressed} // pressed + }; + + int[] colors = new int[]{ + ContextCompat.getColor(ProfileActivity.this, R.color.mastodonC4), + ContextCompat.getColor(ProfileActivity.this, R.color.mastodonC4___), + ContextCompat.getColor(ProfileActivity.this, R.color.mastodonC4), + ContextCompat.getColor(ProfileActivity.this, R.color.mastodonC4) + }; + binding.accountFollow.setBackgroundTintList(new ColorStateList(states, colors)); + binding.accountFollow.setEnabled(true); + //Visibility depending of the relationship + if (relationship != null) { + if (relationship.blocked_by) { + binding.accountFollow.setImageResource(R.drawable.ic_baseline_person_add_24); + binding.accountFollow.setVisibility(View.VISIBLE); + binding.accountFollow.setEnabled(false); + binding.accountFollow.setContentDescription(getString(R.string.action_disabled)); + } + + if (relationship.requested) { + binding.accountFollowRequest.setVisibility(View.VISIBLE); + binding.accountFollow.setImageResource(R.drawable.ic_baseline_hourglass_full_24); + binding.accountFollow.setVisibility(View.VISIBLE); + binding.accountFollow.setContentDescription(getString(R.string.follow_request)); + doAction = action.UNFOLLOW; + } + if (relationship.following) { + binding.accountFollow.setImageResource(R.drawable.ic_baseline_person_remove_24); + binding.accountFollow.setBackgroundTintList(ColorStateList.valueOf(ContextCompat.getColor(ProfileActivity.this, R.color.red_1))); + doAction = action.UNFOLLOW; + binding.accountFollow.setContentDescription(getString(R.string.action_unfollow)); + binding.accountFollow.setVisibility(View.VISIBLE); + } else if (relationship.blocking) { + binding.accountFollow.setBackgroundTintList(ColorStateList.valueOf(ContextCompat.getColor(ProfileActivity.this, R.color.red_1))); + binding.accountFollow.setImageResource(R.drawable.ic_baseline_lock_open_24); + doAction = action.UNBLOCK; + binding.accountFollow.setVisibility(View.VISIBLE); + binding.accountFollow.setContentDescription(getString(R.string.action_unblock)); + } else { + binding.accountFollow.setImageResource(R.drawable.ic_baseline_person_add_24); + doAction = action.FOLLOW; + binding.accountFollow.setVisibility(View.VISIBLE); + binding.accountFollow.setContentDescription(getString(R.string.action_follow)); + } + if (!relationship.following) { + binding.accountNotification.setVisibility(View.GONE); + } else { + binding.accountNotification.setVisibility(View.VISIBLE); + } + if (relationship.notifying) { + binding.accountNotification.setImageResource(R.drawable.ic_baseline_notifications_active_24); + } else { + binding.accountNotification.setImageResource(R.drawable.ic_baseline_notifications_off_24); + } + //Account note + if (relationship.note == null || relationship.note.trim().isEmpty()) { + binding.personalNote.setText(R.string.note_for_account); + } else { + binding.personalNote.setText(relationship.note); + } + binding.personalNote.setOnClickListener(view -> { + AlertDialog.Builder builderInner = new AlertDialog.Builder(ProfileActivity.this, Helper.dialogStyle()); + builderInner.setTitle(R.string.note_for_account); + EditText input = new EditText(ProfileActivity.this); + LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT); + input.setLayoutParams(lp); + input.setSingleLine(false); + input.setText(relationship.note); + input.setImeOptions(EditorInfo.IME_FLAG_NO_ENTER_ACTION); + builderInner.setView(input); + builderInner.setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss()); + builderInner.setPositiveButton(R.string.validate, (dialog, which) -> { + String notes = input.getText().toString().trim(); + binding.accountNote.setText(notes); + accountsVM.updateNote(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, account.id, notes); + dialog.dismiss(); + }); + builderInner.show(); + }); + } + + invalidateOptionsMenu(); + } + + @Override + public boolean onCreateOptionsMenu(@NonNull Menu menu) { + getMenuInflater().inflate(R.menu.activity_profile, menu); + if (account != null) { + final boolean isOwner = account.id.compareToIgnoreCase(BaseMainActivity.currentUserID) == 0; + String[] splitAcct = account.acct.split("@"); + //check if user is from the same instance + if (splitAcct.length <= 1) { //If yes, these entries must be hidden + menu.findItem(R.id.action_follow_instance).setVisible(false); + menu.findItem(R.id.action_block_instance).setVisible(false); + } + if (isOwner) { + menu.findItem(R.id.action_block).setVisible(false); + menu.findItem(R.id.action_report).setVisible(false); + menu.findItem(R.id.action_mute).setVisible(false); + menu.findItem(R.id.action_mention).setVisible(false); + menu.findItem(R.id.action_follow_instance).setVisible(false); + menu.findItem(R.id.action_block_instance).setVisible(false); + menu.findItem(R.id.action_hide_boost).setVisible(false); + menu.findItem(R.id.action_endorse).setVisible(false); + menu.findItem(R.id.action_direct_message).setVisible(false); + menu.findItem(R.id.action_add_to_list).setVisible(false); + } else { + menu.findItem(R.id.action_block).setVisible(true); + menu.findItem(R.id.action_mute).setVisible(true); + menu.findItem(R.id.action_mention).setVisible(true); + } + //Update menu title depending of relationship + if (relationship != null) { + if (!relationship.following) { + menu.findItem(R.id.action_hide_boost).setVisible(false); + menu.findItem(R.id.action_endorse).setVisible(false); + } + if (relationship.blocking) { + menu.findItem(R.id.action_block).setTitle(R.string.action_unblock); + } + if (relationship.muting) { + menu.findItem(R.id.action_mute).setTitle(R.string.action_unmute); + } + if (relationship.endorsed) { + menu.findItem(R.id.action_endorse).setTitle(R.string.unendorse); + } else { + menu.findItem(R.id.action_endorse).setTitle(R.string.endorse); + } + if (relationship.showing_reblogs) { + menu.findItem(R.id.action_hide_boost).setTitle(getString(R.string.hide_boost, account.username)); + } else { + menu.findItem(R.id.action_hide_boost).setTitle(getString(R.string.show_boost, account.username)); + } + } + } + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + int itemId = item.getItemId(); + String[] splitAcct = account.acct.split("@"); + SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(ProfileActivity.this); + AlertDialog.Builder builderInner; + final boolean isOwner = account.id.compareToIgnoreCase(BaseMainActivity.currentUserID) == 0; + final String[] stringArrayConf; + if (isOwner) { + stringArrayConf = getResources().getStringArray(R.array.more_action_owner_confirm); + } else { + stringArrayConf = getResources().getStringArray(R.array.more_action_confirm); + } + action doActionAccount; + if (itemId == android.R.id.home) { + finish(); + return true; + } else if (itemId == R.id.action_follow_instance) { + String finalInstanceName = splitAcct[1]; + ReorderVM reorderVM = new ViewModelProvider(ProfileActivity.this).get(ReorderVM.class); + //Get pinned instances + reorderVM.getPinned().observe(ProfileActivity.this, pinned -> { + boolean alreadyPinned = false; + for (PinnedTimeline pinnedTimeline : pinned.pinnedTimelines) { + if (pinnedTimeline.remoteInstance != null && pinnedTimeline.remoteInstance.host.compareToIgnoreCase(finalInstanceName) == 0) { + alreadyPinned = true; + break; + } + } + if (alreadyPinned) { + Toasty.info(ProfileActivity.this, getString(R.string.toast_instance_already_added), Toast.LENGTH_LONG).show(); + return; + } + if (nodeInfo != null) { + RemoteInstance.InstanceType instanceType; + if (nodeInfo.software.name.compareToIgnoreCase("peertube") == 0) { + instanceType = RemoteInstance.InstanceType.PEERTUBE; + } else if (nodeInfo.software.name.compareToIgnoreCase("pixelfed") == 0) { + instanceType = RemoteInstance.InstanceType.PIXELFED; + } else if (nodeInfo.software.name.compareToIgnoreCase("misskey") == 0) { + instanceType = RemoteInstance.InstanceType.MISSKEY; + } else if (nodeInfo.software.name.compareToIgnoreCase("gnu") == 0) { + instanceType = RemoteInstance.InstanceType.GNU; + } else { + instanceType = RemoteInstance.InstanceType.MASTODON; + } + RemoteInstance remoteInstance = new RemoteInstance(); + remoteInstance.type = instanceType; + remoteInstance.host = finalInstanceName; + PinnedTimeline pinnedTimeline = new PinnedTimeline(); + pinnedTimeline.remoteInstance = remoteInstance; + pinnedTimeline.displayed = true; + pinnedTimeline.type = Timeline.TimeLineEnum.REMOTE; + pinnedTimeline.position = pinned.pinnedTimelines.size(); + pinned.pinnedTimelines.add(pinnedTimeline); + new Thread(() -> { + try { + new Pinned(ProfileActivity.this).updatePinned(pinned); + runOnUiThread(() -> { + Bundle b = new Bundle(); + b.putBoolean(Helper.RECEIVE_REDRAW_TOPBAR, true); + Intent intentBD = new Intent(Helper.BROADCAST_DATA); + intentBD.putExtras(b); + LocalBroadcastManager.getInstance(ProfileActivity.this).sendBroadcast(intentBD); + }); + } catch (DBException e) { + e.printStackTrace(); + } + }).start(); + } + + }); + return true; + } else if (itemId == R.id.action_filter) { + AlertDialog.Builder filterTagDialog = new AlertDialog.Builder(ProfileActivity.this, Helper.dialogStyle()); + Set featuredTagsSet = sharedpreferences.getStringSet(getString(R.string.SET_FEATURED_TAGS), null); + List tags = new ArrayList<>(); + if (featuredTagsSet != null) { + tags = new ArrayList<>(featuredTagsSet); + } + tags.add(0, getString(R.string.no_tags)); + String[] tagsString = tags.toArray(new String[0]); + List finalTags = tags; + String tag = sharedpreferences.getString(getString(R.string.SET_FEATURED_TAG_ACTION), null); + int checkedposition = 0; + int i = 0; + for (String _t : tags) { + if (_t.equals(tag)) + checkedposition = i; + i++; + } + filterTagDialog.setSingleChoiceItems(tagsString, checkedposition, (dialog, item1) -> { + String tag1; + if (item1 == 0) { + tag1 = null; + } else { + tag1 = finalTags.get(item1); + } + SharedPreferences.Editor editor = sharedpreferences.edit(); + editor.putString(getString(R.string.SET_FEATURED_TAG_ACTION), tag1); + editor.apply(); + dialog.dismiss(); + }); + filterTagDialog.show(); + return true; + } else if (itemId == R.id.action_endorse) { + if (relationship != null) + if (relationship.endorsed) { + accountsVM.endorse(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, account.id) + .observe(ProfileActivity.this, relationShip -> this.relationship = relationShip); + } else { + accountsVM.unendorse(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, account.id) + .observe(ProfileActivity.this, relationShip -> this.relationship = relationShip); + } + return true; + } else if (itemId == R.id.action_hide_boost) { + if (relationship != null) + if (relationship.showing_reblogs) { + accountsVM.follow(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, account.id, false, relationship.notifying) + .observe(ProfileActivity.this, relationShip -> this.relationship = relationShip); + } else { + accountsVM.follow(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, account.id, true, relationship.notifying) + .observe(ProfileActivity.this, relationShip -> this.relationship = relationShip); + } + return true; + } else if (itemId == R.id.action_direct_message) { + Intent intent = new Intent(ProfileActivity.this, ComposeActivity.class); + Bundle b = new Bundle(); + b.putSerializable(Helper.ARG_ACCOUNT, account); + b.putString(Helper.ARG_VISIBILITY, "direct"); + intent.putExtras(b); + startActivity(intent); + return true; + } else if (itemId == R.id.action_add_to_list) { + TimelinesVM timelinesVM = new ViewModelProvider(ProfileActivity.this).get(TimelinesVM.class); + timelinesVM.getLists(BaseMainActivity.currentInstance, BaseMainActivity.currentToken) + .observe(ProfileActivity.this, mastodonLists -> { + if (mastodonLists == null || mastodonLists.size() == 0) { + Toasty.info(ProfileActivity.this, getString(R.string.action_lists_empty), Toast.LENGTH_SHORT).show(); + return; + } + accountsVM.getListContainingAccount(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, account.id) + .observe(ProfileActivity.this, mastodonListUserIs -> { + AlertDialog.Builder builderSingle = new AlertDialog.Builder(ProfileActivity.this, Helper.dialogStyle()); + builderSingle.setTitle(getString(R.string.action_lists_add_to)); + builderSingle.setPositiveButton(R.string.close, (dialog, which) -> dialog.dismiss()); + String[] listsId = new String[mastodonLists.size()]; + String[] listsArray = new String[mastodonLists.size()]; + boolean[] presentArray = new boolean[mastodonLists.size()]; + int i = 0; + List userIds = new ArrayList<>(); + userIds.add(account.id); + for (MastodonList mastodonList : mastodonLists) { + listsArray[i] = mastodonList.title; + presentArray[i] = false; + listsId[i] = mastodonList.id; + for (MastodonList mastodonListPresent : mastodonListUserIs) { + if (mastodonList.id.equalsIgnoreCase(mastodonListPresent.id)) { + presentArray[i] = true; + break; + } + } + i++; + } + builderSingle.setMultiChoiceItems(listsArray, presentArray, (dialog, which, isChecked) -> { + if (!relationship.following) { + accountsVM.follow(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, account.id, true, false) + .observe(ProfileActivity.this, newRelationShip -> { + relationship = newRelationShip; + updateAccount(); + if (isChecked) { + timelinesVM.addAccountsList(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, listsId[which], userIds); + } else { + timelinesVM.deleteAccountsList(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, listsId[which], userIds); + } + }); + } else { + if (isChecked) { + timelinesVM.addAccountsList(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, listsId[which], userIds); + } else { + timelinesVM.deleteAccountsList(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, listsId[which], userIds); + } + } + + }); + builderSingle.show(); + }); + + }); + return true; + } else if (itemId == R.id.action_open_browser) { + if (account.url != null) { + if (!account.url.toLowerCase().startsWith("http://") && !account.url.toLowerCase().startsWith("https://")) + account.url = "http://" + account.url; + Helper.openBrowser(ProfileActivity.this, account.url); + } + return true; + } else if (itemId == R.id.action_mention) { + Intent intent; + Bundle b; + intent = new Intent(ProfileActivity.this, ComposeActivity.class); + b = new Bundle(); + b.putSerializable(Helper.ARG_ACCOUNT, account.acct); + intent.putExtras(b); + startActivity(intent); + return true; + } else if (itemId == R.id.action_mute) { + if (relationship.muting) { + builderInner = new AlertDialog.Builder(ProfileActivity.this, Helper.dialogStyle()); + builderInner.setTitle(stringArrayConf[4]); + doActionAccount = action.UNMUTE; + } else { + builderInner = new AlertDialog.Builder(ProfileActivity.this, Helper.dialogStyle()); + builderInner.setTitle(stringArrayConf[0]); + doActionAccount = action.MUTE; + } + } else if (itemId == R.id.action_report) { + builderInner = new AlertDialog.Builder(ProfileActivity.this, Helper.dialogStyle()); + builderInner.setTitle(R.string.report_account); + //Text for report + EditText input = new EditText(ProfileActivity.this); + LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT); + input.setLayoutParams(lp); + builderInner.setView(input); + builderInner.setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss()); + builderInner.setPositiveButton(R.string.yes, (dialog, which) -> { + String comment = null; + if (input.getText() != null) + comment = input.getText().toString(); + accountsVM.report(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, account.id, null, null, null, comment, false); + dialog.dismiss(); + }); + builderInner.show(); + return true; + } else if (itemId == R.id.action_block) { + builderInner = new AlertDialog.Builder(ProfileActivity.this, Helper.dialogStyle()); + if (relationship.blocking) { + builderInner.setTitle(stringArrayConf[5]); + doActionAccount = action.UNBLOCK; + } else { + builderInner.setTitle(stringArrayConf[1]); + doActionAccount = action.BLOCK; + } + } else if (itemId == R.id.action_block_instance) { + builderInner = new AlertDialog.Builder(ProfileActivity.this, Helper.dialogStyle()); + doActionAccount = action.BLOCK_DOMAIN; + String domain = account.acct.split("@")[1]; + builderInner.setMessage(getString(R.string.block_domain_confirm_message, domain)); + } else { + return true; + } + builderInner.setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss()); + builderInner.setPositiveButton(R.string.yes, (dialog, which) -> { + String target; + if (item.getItemId() == R.id.action_block_instance) { + target = account.acct.split("@")[1]; + } else { + target = account.id; + } + switch (doActionAccount) { + case MUTE: + accountsVM.mute(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, target, true, 0) + .observe(ProfileActivity.this, relationShip -> { + this.relationship = relationShip; + updateAccount(); + }); + break; + case UNMUTE: + accountsVM.unmute(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, target) + .observe(ProfileActivity.this, relationShip -> { + this.relationship = relationShip; + updateAccount(); + }); + break; + case BLOCK: + accountsVM.block(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, target) + .observe(ProfileActivity.this, relationShip -> { + this.relationship = relationShip; + updateAccount(); + }); + break; + case UNBLOCK: + accountsVM.unblock(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, target) + .observe(ProfileActivity.this, relationShip -> { + this.relationship = relationShip; + updateAccount(); + }); + break; + case BLOCK_DOMAIN: + accountsVM.addDomainBlocks(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, target); + break; + } + dialog.dismiss(); + }); + builderInner.show(); + return true; + } + + @Override + public void onDestroy() { + if (scheduledExecutorService != null) { + scheduledExecutorService.shutdownNow(); + scheduledExecutorService = null; + } + LocalBroadcastManager.getInstance(ProfileActivity.this).unregisterReceiver(broadcast_data); + super.onDestroy(); + } + + public enum action { + FOLLOW, + UNFOLLOW, + BLOCK, + UNBLOCK, + NOTHING, + MUTE, + UNMUTE, + REPORT, + BLOCK_DOMAIN + } + + +} diff --git a/app/src/main/java/app/fedilab/android/activities/ProxyActivity.java b/app/src/main/java/app/fedilab/android/activities/ProxyActivity.java new file mode 100644 index 000000000..f039f13d9 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/activities/ProxyActivity.java @@ -0,0 +1,108 @@ +package app.fedilab.android.activities; +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + + +import android.content.SharedPreferences; +import android.os.Bundle; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; + +import androidx.preference.PreferenceManager; + +import app.fedilab.android.R; +import app.fedilab.android.databinding.ActivityProxyBinding; + + +public class ProxyActivity extends BaseActivity { + + private ActivityProxyBinding binding; + private int position; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(ProxyActivity.this); + binding = ActivityProxyBinding.inflate(getLayoutInflater()); + setContentView(binding.getRoot()); + getWindow().setLayout(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); + if (getSupportActionBar() != null) + getSupportActionBar().hide(); + + //Enable proxy + boolean enable_proxy = sharedpreferences.getBoolean(getString(R.string.SET_PROXY_ENABLED), false); + binding.enableProxy.setChecked(enable_proxy); + position = 0; + String hostVal = sharedpreferences.getString(getString(R.string.SET_PROXY_HOST), "127.0.0.1"); + int portVal = sharedpreferences.getInt(getString(R.string.SET_PROXY_PORT), 8118); + final String login = sharedpreferences.getString(getString(R.string.SET_PROXY_LOGIN), null); + final String pwd = sharedpreferences.getString(getString(R.string.SET_PROXY_PASSWORD), null); + if (hostVal.length() > 0) { + binding.host.setText(hostVal); + } + binding.port.setText(String.valueOf(portVal)); + if (login != null && login.length() > 0) { + binding.proxyLogin.setText(login); + } + if (pwd != null && binding.proxyPassword.length() > 0) { + binding.proxyPassword.setText(pwd); + } + ArrayAdapter adapterTrans = ArrayAdapter.createFromResource(ProxyActivity.this, + R.array.proxy_type_choice, android.R.layout.simple_spinner_dropdown_item); + binding.type.setAdapter(adapterTrans); + binding.type.setSelection(sharedpreferences.getInt(getString(R.string.SET_PROXY_TYPE), 0), false); + binding.type.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int p, long id) { + position = p; + } + + @Override + public void onNothingSelected(AdapterView parent) { + + } + }); + binding.setProxySave.setOnClickListener(view -> { + String hostVal1 = binding.host.getText().toString().trim(); + String portVal1 = binding.port.getText().toString().trim(); + String proxy_loginVal = binding.proxyLogin.getText().toString().trim(); + String proxy_passwordVal = binding.proxyPassword.getText().toString().trim(); + SharedPreferences.Editor editor = sharedpreferences.edit(); + editor.putBoolean(getString(R.string.SET_PROXY_ENABLED), binding.enableProxy.isChecked()); + editor.putInt(getString(R.string.SET_PROXY_TYPE), position); + editor.putString(getString(R.string.SET_PROXY_HOST), hostVal1); + if (portVal1.matches("\\d+")) + editor.putInt(getString(R.string.SET_PROXY_PORT), Integer.parseInt(portVal1)); + editor.putString(getString(R.string.SET_PROXY_LOGIN), proxy_loginVal); + editor.putString(getString(R.string.SET_PROXY_PASSWORD), proxy_passwordVal); + editor.apply(); + finish(); + }); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == android.R.id.home) { + finish(); + return true; + } + return super.onOptionsItemSelected(item); + } + + +} diff --git a/app/src/main/java/app/fedilab/android/activities/ReorderTimelinesActivity.java b/app/src/main/java/app/fedilab/android/activities/ReorderTimelinesActivity.java new file mode 100644 index 000000000..faac09a01 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/activities/ReorderTimelinesActivity.java @@ -0,0 +1,352 @@ +package app.fedilab.android.activities; +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + + +import static app.fedilab.android.helper.PinnedTimelineHelper.sortPositionAsc; + +import android.content.Intent; +import android.graphics.Paint; +import android.os.Bundle; +import android.os.Handler; +import android.text.Editable; +import android.text.InputFilter; +import android.text.TextWatcher; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.WindowManager; +import android.view.inputmethod.InputMethodManager; +import android.widget.ArrayAdapter; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import androidx.lifecycle.ViewModelProvider; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; +import androidx.recyclerview.widget.ItemTouchHelper; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.concurrent.TimeUnit; + +import app.fedilab.android.R; +import app.fedilab.android.client.entities.InstanceSocial; +import app.fedilab.android.client.entities.Pinned; +import app.fedilab.android.client.entities.Timeline; +import app.fedilab.android.client.entities.app.PinnedTimeline; +import app.fedilab.android.client.entities.app.RemoteInstance; +import app.fedilab.android.databinding.ActivityReorderTabsBinding; +import app.fedilab.android.databinding.PopupSearchInstanceBinding; +import app.fedilab.android.exception.DBException; +import app.fedilab.android.helper.Helper; +import app.fedilab.android.helper.ThemeHelper; +import app.fedilab.android.helper.itemtouchhelper.OnStartDragListener; +import app.fedilab.android.helper.itemtouchhelper.OnUndoListener; +import app.fedilab.android.helper.itemtouchhelper.SimpleItemTouchHelperCallback; +import app.fedilab.android.ui.drawer.ReorderTabAdapter; +import app.fedilab.android.viewmodel.mastodon.InstanceSocialVM; +import app.fedilab.android.viewmodel.mastodon.ReorderVM; +import es.dmoral.toasty.Toasty; +import okhttp3.Call; +import okhttp3.Callback; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; + + +public class ReorderTimelinesActivity extends BaseActivity implements OnStartDragListener, OnUndoListener { + + + private ItemTouchHelper touchHelper; + private ReorderTabAdapter adapter; + private boolean searchInstanceRunning; + private String oldSearch; + private Pinned pinned; + private ActivityReorderTabsBinding binding; + private boolean changes; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + ThemeHelper.applyThemeBar(this); + binding = ActivityReorderTabsBinding.inflate(getLayoutInflater()); + setContentView(binding.getRoot()); + searchInstanceRunning = false; + if (getSupportActionBar() != null) + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + + changes = false; + ReorderVM reorderVM = new ViewModelProvider(ReorderTimelinesActivity.this).get(ReorderVM.class); + reorderVM.getPinned().observe(ReorderTimelinesActivity.this, _pinned -> { + this.pinned = _pinned; + if (this.pinned == null) { + this.pinned = new Pinned(); + this.pinned.pinnedTimelines = new ArrayList<>(); + } + sortPositionAsc(this.pinned.pinnedTimelines); + adapter = new ReorderTabAdapter(this.pinned, ReorderTimelinesActivity.this, ReorderTimelinesActivity.this); + ItemTouchHelper.Callback callback = + new SimpleItemTouchHelperCallback(adapter); + touchHelper = new ItemTouchHelper(callback); + touchHelper.attachToRecyclerView(binding.lvReorderTabs); + binding.lvReorderTabs.setAdapter(adapter); + LinearLayoutManager mLayoutManager = new LinearLayoutManager(ReorderTimelinesActivity.this); + binding.lvReorderTabs.setLayoutManager(mLayoutManager); + }); + + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == android.R.id.home) { + finish(); + return true; + } else if (item.getItemId() == R.id.action_add_timeline) { + addInstance(); + } + + return super.onOptionsItemSelected(item); + } + + + @Override + public boolean onCreateOptionsMenu(@NonNull Menu menu) { + getMenuInflater().inflate(R.menu.menu_reorder, menu); + return super.onCreateOptionsMenu(menu); + } + + private void addInstance() { + AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(ReorderTimelinesActivity.this, Helper.dialogStyle()); + PopupSearchInstanceBinding popupSearchInstanceBinding = PopupSearchInstanceBinding.inflate(getLayoutInflater()); + dialogBuilder.setView(popupSearchInstanceBinding.getRoot()); + popupSearchInstanceBinding.setAttachmentGroup.setOnCheckedChangeListener((group, checkedId) -> { + if (checkedId == R.id.twitter_accounts) { + popupSearchInstanceBinding.searchInstance.setHint(R.string.list_of_twitter_accounts); + } else { + popupSearchInstanceBinding.searchInstance.setHint(R.string.instance); + } + }); + popupSearchInstanceBinding.searchInstance.setFilters(new InputFilter[]{new InputFilter.LengthFilter(60)}); + dialogBuilder.setPositiveButton(R.string.validate, (dialog, id) -> { + String instanceName = popupSearchInstanceBinding.searchInstance.getText().toString().trim().replace("@", ""); + new Thread(() -> { + String url = null; + if (popupSearchInstanceBinding.setAttachmentGroup.getCheckedRadioButtonId() == R.id.mastodon_instance) + url = "https://" + instanceName + "/api/v1/timelines/public?local=true"; + else if (popupSearchInstanceBinding.setAttachmentGroup.getCheckedRadioButtonId() == R.id.peertube_instance) + url = "https://" + instanceName + "/api/v1/videos/"; + else if (popupSearchInstanceBinding.setAttachmentGroup.getCheckedRadioButtonId() == R.id.pixelfed_instance) { + url = "https://" + instanceName + "/api/v1/timelines/public"; + } else if (popupSearchInstanceBinding.setAttachmentGroup.getCheckedRadioButtonId() == R.id.misskey_instance) { + url = "https://" + instanceName + "/api/notes/local-timeline"; + } else if (popupSearchInstanceBinding.setAttachmentGroup.getCheckedRadioButtonId() == R.id.gnu_instance) { + url = "https://" + instanceName + "/api/statuses/public_timeline.json"; + } + OkHttpClient client = new OkHttpClient.Builder() + .connectTimeout(10, TimeUnit.SECONDS) + .writeTimeout(10, TimeUnit.SECONDS) + .proxy(Helper.getProxy(getApplication().getApplicationContext())) + .readTimeout(10, TimeUnit.SECONDS).build(); + Request request; + if (url != null) { + request = new Request.Builder() + .url(url) + .build(); + client.newCall(request).enqueue(new Callback() { + @Override + public void onFailure(@NonNull Call call, @NonNull IOException e) { + e.printStackTrace(); + runOnUiThread(() -> Toasty.warning(ReorderTimelinesActivity.this, getString(R.string.toast_instance_unavailable), Toast.LENGTH_LONG).show()); + } + + @Override + public void onResponse(@NonNull Call call, @NonNull final Response response) { + if (!response.isSuccessful()) { + runOnUiThread(() -> { + dialog.dismiss(); + RemoteInstance.InstanceType instanceType = null; + if (popupSearchInstanceBinding.setAttachmentGroup.getCheckedRadioButtonId() == R.id.mastodon_instance) { + instanceType = RemoteInstance.InstanceType.MASTODON; + } else if (popupSearchInstanceBinding.setAttachmentGroup.getCheckedRadioButtonId() == R.id.peertube_instance) { + instanceType = RemoteInstance.InstanceType.PEERTUBE; + } else if (popupSearchInstanceBinding.setAttachmentGroup.getCheckedRadioButtonId() == R.id.pixelfed_instance) { + instanceType = RemoteInstance.InstanceType.PIXELFED; + } else if (popupSearchInstanceBinding.setAttachmentGroup.getCheckedRadioButtonId() == R.id.misskey_instance) { + instanceType = RemoteInstance.InstanceType.MISSKEY; + } else if (popupSearchInstanceBinding.setAttachmentGroup.getCheckedRadioButtonId() == R.id.gnu_instance) { + instanceType = RemoteInstance.InstanceType.GNU; + } else if (popupSearchInstanceBinding.setAttachmentGroup.getCheckedRadioButtonId() == R.id.twitter_accounts) { + instanceType = RemoteInstance.InstanceType.NITTER; + } + RemoteInstance remoteInstance = new RemoteInstance(); + remoteInstance.type = instanceType; + remoteInstance.host = instanceName; + PinnedTimeline pinnedTimeline = new PinnedTimeline(); + pinnedTimeline.remoteInstance = remoteInstance; + pinnedTimeline.displayed = true; + pinnedTimeline.type = Timeline.TimeLineEnum.REMOTE; + pinnedTimeline.position = pinned.pinnedTimelines.size(); + pinned.pinnedTimelines.add(pinnedTimeline); + try { + new Pinned(ReorderTimelinesActivity.this).updatePinned(pinned); + changes = true; + adapter.notifyItemInserted(pinned.pinnedTimelines.size()); + } catch (DBException e) { + e.printStackTrace(); + } + }); + } else { + runOnUiThread(() -> Toasty.warning(ReorderTimelinesActivity.this, getString(R.string.toast_instance_unavailable), Toast.LENGTH_LONG).show()); + } + } + }); + } else { + runOnUiThread(() -> Toasty.warning(ReorderTimelinesActivity.this, getString(R.string.toast_instance_unavailable), Toast.LENGTH_LONG).show()); + } + }).start(); + }); + AlertDialog alertDialog = dialogBuilder.create(); + alertDialog.setOnDismissListener(dialogInterface -> { + //Hide keyboard + InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE); + assert imm != null; + imm.hideSoftInputFromWindow(popupSearchInstanceBinding.searchInstance.getWindowToken(), 0); + }); + if (alertDialog.getWindow() != null) + alertDialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE); + alertDialog.show(); + + popupSearchInstanceBinding.searchInstance.setOnItemClickListener((parent, view1, position, id) -> oldSearch = parent.getItemAtPosition(position).toString().trim()); + + popupSearchInstanceBinding.searchInstance.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + + } + + @Override + public void afterTextChanged(Editable s) { + + if (s.length() > 2 && !searchInstanceRunning) { + String query = s.toString().trim(); + if (query.startsWith("http://")) { + query = query.replace("http://", ""); + } + if (query.startsWith("https://")) { + query = query.replace("https://", ""); + } + if (oldSearch == null || !oldSearch.equals(s.toString().trim())) { + searchInstanceRunning = true; + InstanceSocialVM instanceSocialVM = new ViewModelProvider(ReorderTimelinesActivity.this).get(InstanceSocialVM.class); + instanceSocialVM.getInstances(query).observe(ReorderTimelinesActivity.this, instanceSocialList -> { + popupSearchInstanceBinding.searchInstance.setAdapter(null); + String[] instances = new String[instanceSocialList.instances.size()]; + int j = 0; + for (InstanceSocial.Instance instance : instanceSocialList.instances) { + instances[j] = instance.name; + j++; + } + ArrayAdapter arrayAdapter = + new ArrayAdapter<>(ReorderTimelinesActivity.this, android.R.layout.simple_list_item_1, instances); + popupSearchInstanceBinding.searchInstance.setAdapter(arrayAdapter); + if (popupSearchInstanceBinding.searchInstance.hasFocus() && !isFinishing()) + popupSearchInstanceBinding.searchInstance.showDropDown(); + if (oldSearch != null && oldSearch.equals(popupSearchInstanceBinding.searchInstance.getText().toString())) { + popupSearchInstanceBinding.searchInstance.dismissDropDown(); + } + + oldSearch = s.toString().trim(); + searchInstanceRunning = false; + }); + } + } + } + }); + + } + + @Override + protected void onPause() { + super.onPause(); + if (changes) { + //Update menu + Bundle b = new Bundle(); + b.putBoolean(Helper.RECEIVE_REDRAW_TOPBAR, true); + Intent intentBD = new Intent(Helper.BROADCAST_DATA); + intentBD.putExtras(b); + LocalBroadcastManager.getInstance(ReorderTimelinesActivity.this).sendBroadcast(intentBD); + } + } + + @Override + public void onStartDrag(RecyclerView.ViewHolder viewHolder) { + touchHelper.startDrag(viewHolder); + } + + + @Override + public void onUndo(PinnedTimeline pinnedTimeline, int position) { + binding.undoContainer.setVisibility(View.VISIBLE); + switch (pinnedTimeline.type) { + case TAG: + binding.undoMessage.setText(R.string.reorder_tag_removed); + break; + case REMOTE: + binding.undoMessage.setText(R.string.reorder_instance_removed); + break; + case LIST: + binding.undoMessage.setText(R.string.reorder_list_deleted); + break; + } + binding.undoAction.setPaintFlags(binding.undoAction.getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG); + Runnable runnable = () -> { + binding.undoContainer.setVisibility(View.GONE); + //change position of pinned that are after the removed item + for (int i = pinnedTimeline.position + 1; i < pinned.pinnedTimelines.size(); i++) { + pinned.pinnedTimelines.get(i).position -= 1; + } + pinned.pinnedTimelines.remove(pinnedTimeline); + adapter.notifyItemRemoved(position); + try { + new Pinned(ReorderTimelinesActivity.this).updatePinned(pinned); + changes = true; + } catch (DBException e) { + e.printStackTrace(); + } + }; + Handler handler = new Handler(); + handler.postDelayed(runnable, 4000); + binding.undoAction.setOnClickListener(v -> { + pinned.pinnedTimelines.add(position, pinnedTimeline); + adapter.notifyItemInserted(position); + binding.undoContainer.setVisibility(View.GONE); + handler.removeCallbacks(runnable); + }); + } + + @Override + public void onStop() { + super.onStop(); + } + + +} diff --git a/app/src/main/java/app/fedilab/android/activities/ReportActivity.java b/app/src/main/java/app/fedilab/android/activities/ReportActivity.java new file mode 100644 index 000000000..dcf56db43 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/activities/ReportActivity.java @@ -0,0 +1,327 @@ +package app.fedilab.android.activities; +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import android.os.Bundle; +import android.view.MenuItem; +import android.view.View; +import android.widget.RadioButton; + +import androidx.annotation.Nullable; +import androidx.appcompat.widget.LinearLayoutCompat; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentTransaction; +import androidx.lifecycle.ViewModelProvider; +import androidx.recyclerview.widget.LinearLayoutManager; + +import java.util.ArrayList; +import java.util.List; + +import app.fedilab.android.BaseMainActivity; +import app.fedilab.android.R; +import app.fedilab.android.client.entities.Timeline; +import app.fedilab.android.client.mastodon.entities.Account; +import app.fedilab.android.client.mastodon.entities.RelationShip; +import app.fedilab.android.client.mastodon.entities.Status; +import app.fedilab.android.databinding.ActivityReportBinding; +import app.fedilab.android.helper.Helper; +import app.fedilab.android.helper.ThemeHelper; +import app.fedilab.android.ui.drawer.RulesAdapter; +import app.fedilab.android.ui.fragment.timeline.FragmentMastodonTimeline; +import app.fedilab.android.viewmodel.mastodon.AccountsVM; +import es.dmoral.toasty.Toasty; + + +public class ReportActivity extends BaseActivity { + + private ActivityReportBinding binding; + private Status status; + private Account account; + private AccountsVM accountsVM; + private RelationShip relationShip; + private List statusIds; + private List ruleIds; + private String comment; + private boolean forward; + private FragmentMastodonTimeline fragment; + private RulesAdapter rulesAdapter; + private String category; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + ThemeHelper.applyThemeBar(this); + binding = ActivityReportBinding.inflate(getLayoutInflater()); + + setContentView(binding.getRoot()); + + if (getSupportActionBar() != null) + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + + Bundle b = getIntent().getExtras(); + if (b != null) { + status = (Status) b.getSerializable(Helper.ARG_STATUS); + account = (Account) b.getSerializable(Helper.ARG_ACCOUNT); + } + if (account == null && status != null) { + account = status.account; + } + //The entry view + show(binding.screenReason); + + setTitle(getString(R.string.report_title, "@" + account.acct)); + + binding.val1.setOnClickListener(v -> { + binding.actionButton.setEnabled(true); + setChecked(binding.val1); + }); + binding.val2.setOnClickListener(v -> { + binding.actionButton.setEnabled(true); + setChecked(binding.val2); + }); + binding.val3.setOnClickListener(v -> { + binding.actionButton.setEnabled(true); + setChecked(binding.val3); + }); + binding.val4.setOnClickListener(v -> { + binding.actionButton.setEnabled(true); + setChecked(binding.val4); + }); + + binding.val1Container.setOnClickListener(v -> { + binding.actionButton.setEnabled(true); + setChecked(binding.val1); + }); + binding.val2Container.setOnClickListener(v -> { + binding.actionButton.setEnabled(true); + setChecked(binding.val2); + }); + binding.val3Container.setOnClickListener(v -> { + binding.actionButton.setEnabled(true); + setChecked(binding.val3); + }); + binding.val4Container.setOnClickListener(v -> { + binding.actionButton.setEnabled(true); + setChecked(binding.val4); + }); + + + accountsVM = new ViewModelProvider(ReportActivity.this).get(AccountsVM.class); + binding.actionButton.setOnClickListener(v -> { + if (binding.screenReason.getVisibility() == View.VISIBLE) { + if (binding.val1.isChecked()) { + show(binding.screenIdontlike); + switchToIDontLike(); + } else if (binding.val2.isChecked()) { + show(binding.screenSpam); + switchToSpam(); + } else if (binding.val3.isChecked()) { + show(binding.screenViolateRules); + switchToRules(); + } else if (binding.val4.isChecked()) { + show(binding.screenSomethingElse); + switchToSomethingElse(); + } + } + }); + } + + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == android.R.id.home) { + finish(); + return true; + } + return super.onOptionsItemSelected(item); + } + + + private void switchToRules() { + rulesAdapter = new RulesAdapter(BaseMainActivity.instanceInfo.rules); + binding.lvVr.setAdapter(rulesAdapter); + binding.lvVr.setLayoutManager(new LinearLayoutManager(ReportActivity.this)); + binding.actionButton.setText(R.string.next); + binding.actionButton.setOnClickListener(v -> { + category = "violation"; + show(binding.screenSomethingElse); + switchToSomethingElse(); + }); + } + + private void switchToIDontLike() { + List ids = new ArrayList<>(); + ids.add(account.id); + binding.unfollowTitle.setText(getString(R.string.report_1_unfollow_title, "@" + account.acct)); + binding.muteTitle.setText(getString(R.string.report_1_mute_title, "@" + account.acct)); + binding.blockTitle.setText(getString(R.string.report_1_block_title, "@" + account.acct)); + accountsVM.getRelationships(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, ids) + .observe(ReportActivity.this, relationShips -> { + if (relationShips != null && relationShips.size() > 0) { + relationShip = relationShips.get(0); + if (relationShip.following) { + binding.groupUnfollow.setVisibility(View.VISIBLE); + } else { + binding.groupUnfollow.setVisibility(View.GONE); + } + if (relationShip.blocking) { + binding.groupBlock.setVisibility(View.GONE); + } else { + binding.groupBlock.setVisibility(View.VISIBLE); + } + if (relationShip.muting) { + binding.groupMute.setVisibility(View.GONE); + } else { + binding.groupMute.setVisibility(View.VISIBLE); + } + } + binding.actionUnfollow.setOnClickListener(v -> accountsVM.unfollow(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, account.id) + .observe(ReportActivity.this, rsUnfollow -> { + if (rsUnfollow != null) { + relationShip = rsUnfollow; + Toasty.info(ReportActivity.this, getString(R.string.toast_unfollow), Toasty.LENGTH_LONG).show(); + binding.groupUnfollow.setVisibility(View.GONE); + } + })); + binding.actionMute.setOnClickListener(v -> accountsVM.mute(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, account.id, null, null) + .observe(ReportActivity.this, rsMute -> { + if (rsMute != null) { + relationShip = rsMute; + Toasty.info(ReportActivity.this, getString(R.string.toast_mute), Toasty.LENGTH_LONG).show(); + binding.groupMute.setVisibility(View.GONE); + } + })); + binding.actionBlock.setOnClickListener(v -> accountsVM.block(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, account.id) + .observe(ReportActivity.this, rsBlock -> { + if (rsBlock != null) { + relationShip = rsBlock; + Toasty.info(ReportActivity.this, getString(R.string.toast_block), Toasty.LENGTH_LONG).show(); + binding.groupBlock.setVisibility(View.GONE); + } + })); + + }); + binding.actionButton.setText(R.string.done); + binding.actionButton.setOnClickListener(v -> finish()); + } + + + private void switchToSpam() { + + fragment = new FragmentMastodonTimeline(); + Bundle bundle = new Bundle(); + bundle.putSerializable(Helper.ARG_TIMELINE_TYPE, Timeline.TimeLineEnum.ACCOUNT_TIMELINE); + bundle.putSerializable(Helper.ARG_ACCOUNT, account); + //Set to display statuses with less options + bundle.putBoolean(Helper.ARG_MINIFIED, true); + if (status != null) { + status.isChecked = true; + bundle.putSerializable(Helper.ARG_STATUS_REPORT, status); + } + fragment.setArguments(bundle); + FragmentManager fragmentManager = getSupportFragmentManager(); + FragmentTransaction fragmentTransaction = + fragmentManager.beginTransaction(); + fragmentTransaction.replace(R.id.fram_spam_container, fragment); + fragmentTransaction.commit(); + + binding.actionButton.setText(R.string.next); + binding.actionButton.setOnClickListener(v -> { + category = "spam"; + switchToMoreInfo(); + }); + } + + private void switchToSomethingElse() { + + fragment = new FragmentMastodonTimeline(); + Bundle bundle = new Bundle(); + bundle.putSerializable(Helper.ARG_TIMELINE_TYPE, Timeline.TimeLineEnum.ACCOUNT_TIMELINE); + bundle.putSerializable(Helper.ARG_ACCOUNT, account); + //Set to display statuses with less options + bundle.putBoolean(Helper.ARG_MINIFIED, true); + if (status != null) { + status.isChecked = true; + bundle.putSerializable(Helper.ARG_STATUS_REPORT, status); + } + fragment.setArguments(bundle); + FragmentManager fragmentManager = getSupportFragmentManager(); + FragmentTransaction fragmentTransaction = + fragmentManager.beginTransaction(); + fragmentTransaction.replace(R.id.fram_se_container, fragment); + fragmentTransaction.commit(); + + binding.actionButton.setText(R.string.next); + binding.actionButton.setOnClickListener(v -> { + if (category == null) { + category = "other"; + } + switchToMoreInfo(); + }); + } + + private void switchToMoreInfo() { + + show(binding.screenMoreDetails); + String[] domains = account.acct.split("@"); + if (domains.length > 1) { + binding.forward.setOnCheckedChangeListener((compoundButton, checked) -> forward = checked); + binding.forward.setText(getString(R.string.report_more_forward, domains[1])); + } else { + forward = false; + binding.forwardBlock.setVisibility(View.GONE); + } + binding.actionButton.setText(R.string.done); + binding.actionButton.setOnClickListener(v -> { + if (rulesAdapter != null) { + ruleIds = rulesAdapter.getChecked(); + } + if (fragment != null) { + statusIds = fragment.getCheckedStatusesId(); + } + comment = binding.reportMessage.getText().toString(); + binding.actionButton.setEnabled(false); + accountsVM.report(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, account.id, category, statusIds, ruleIds, comment, forward) + .observe(ReportActivity.this, report -> { + Toasty.success(ReportActivity.this, R.string.report_sent, Toasty.LENGTH_LONG).show(); + finish(); + }); + }); + } + + + /** + * Hide all views and display the wanted one + * + * @param linearLayoutCompat LinearLayoutCompat - One of the report page + */ + private void show(LinearLayoutCompat linearLayoutCompat) { + binding.screenReason.setVisibility(View.GONE); + binding.screenIdontlike.setVisibility(View.GONE); + binding.screenSpam.setVisibility(View.GONE); + binding.screenViolateRules.setVisibility(View.GONE); + binding.screenSomethingElse.setVisibility(View.GONE); + binding.screenMoreDetails.setVisibility(View.GONE); + linearLayoutCompat.setVisibility(View.VISIBLE); + } + + private void setChecked(RadioButton radioButton) { + binding.val1.setChecked(false); + binding.val2.setChecked(false); + binding.val3.setChecked(false); + binding.val4.setChecked(false); + radioButton.setChecked(true); + } +} diff --git a/app/src/main/java/app/fedilab/android/activities/ScheduledActivity.java b/app/src/main/java/app/fedilab/android/activities/ScheduledActivity.java new file mode 100644 index 000000000..98b6686db --- /dev/null +++ b/app/src/main/java/app/fedilab/android/activities/ScheduledActivity.java @@ -0,0 +1,88 @@ +package app.fedilab.android.activities; +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import android.os.Bundle; +import android.view.MenuItem; + +import androidx.annotation.Nullable; +import androidx.appcompat.app.ActionBar; + +import com.google.android.material.tabs.TabLayout; + +import app.fedilab.android.BaseMainActivity; +import app.fedilab.android.R; +import app.fedilab.android.databinding.ActivityScheduledBinding; +import app.fedilab.android.helper.MastodonHelper; +import app.fedilab.android.helper.ThemeHelper; +import app.fedilab.android.ui.pageadapter.FedilabScheduledPageAdapter; + +public class ScheduledActivity extends BaseActivity { + + private ActivityScheduledBinding binding; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + ThemeHelper.applyTheme(this); + binding = ActivityScheduledBinding.inflate(getLayoutInflater()); + setContentView(binding.getRoot()); + setSupportActionBar(binding.toolbar); + ActionBar actionBar = getSupportActionBar(); + //Remove title + if (actionBar != null) { + actionBar.setDisplayShowTitleEnabled(false); + } + if (getSupportActionBar() != null) { + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + getSupportActionBar().setDisplayShowHomeEnabled(true); + } + + MastodonHelper.loadPPMastodon(binding.profilePicture, BaseMainActivity.accountWeakReference.get().mastodon_account); + binding.title.setText(R.string.scheduled); + binding.scheduleTablayout.addTab(binding.scheduleTablayout.newTab().setText(getString(R.string.toots_server))); + binding.scheduleTablayout.addTab(binding.scheduleTablayout.newTab().setText(getString(R.string.toots_client))); + binding.scheduleTablayout.addTab(binding.scheduleTablayout.newTab().setText(getString(R.string.reblog))); + + binding.scheduleViewpager.setAdapter(new FedilabScheduledPageAdapter(getSupportFragmentManager())); + binding.scheduleViewpager.setOffscreenPageLimit(3); + binding.scheduleViewpager.addOnPageChangeListener(new TabLayout.TabLayoutOnPageChangeListener(binding.scheduleTablayout)); + binding.scheduleTablayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() { + @Override + public void onTabSelected(TabLayout.Tab tab) { + binding.scheduleViewpager.setCurrentItem(tab.getPosition()); + } + + @Override + public void onTabUnselected(TabLayout.Tab tab) { + + } + + @Override + public void onTabReselected(TabLayout.Tab tab) { + + } + }); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == android.R.id.home) { + finish(); + return true; + } + return super.onOptionsItemSelected(item); + } +} diff --git a/app/src/main/java/app/fedilab/android/activities/SearchResultTabActivity.java b/app/src/main/java/app/fedilab/android/activities/SearchResultTabActivity.java new file mode 100644 index 000000000..f493cc609 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/activities/SearchResultTabActivity.java @@ -0,0 +1,240 @@ +package app.fedilab.android.activities; +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import android.app.SearchManager; +import android.content.Context; +import android.os.Bundle; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.ViewGroup; +import android.view.inputmethod.InputMethodManager; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.appcompat.widget.SearchView; +import androidx.core.content.ContextCompat; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentStatePagerAdapter; +import androidx.viewpager.widget.PagerAdapter; +import androidx.viewpager.widget.ViewPager; + +import com.google.android.material.tabs.TabLayout; + +import org.jetbrains.annotations.NotNull; + +import app.fedilab.android.R; +import app.fedilab.android.databinding.ActivitySearchResultTabsBinding; +import app.fedilab.android.helper.Helper; +import app.fedilab.android.helper.ThemeHelper; +import app.fedilab.android.ui.fragment.timeline.FragmentMastodonAccount; +import app.fedilab.android.ui.fragment.timeline.FragmentMastodonTag; +import app.fedilab.android.ui.fragment.timeline.FragmentMastodonTimeline; +import es.dmoral.toasty.Toasty; + + +public class SearchResultTabActivity extends BaseActivity { + + + private String search; + private ActivitySearchResultTabsBinding binding; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + ThemeHelper.applyThemeBar(this); + binding = ActivitySearchResultTabsBinding.inflate(getLayoutInflater()); + setContentView(binding.getRoot()); + + Bundle b = getIntent().getExtras(); + if (b != null) { + search = b.getString(Helper.ARG_SEARCH_KEYWORD, null); + + } + if (search == null) { + Toasty.error(SearchResultTabActivity.this, getString(R.string.toast_error_search), Toast.LENGTH_LONG).show(); + finish(); + return; + } + + if (getSupportActionBar() != null) + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + setTitle(search); + binding.searchTabLayout.addTab(binding.searchTabLayout.newTab().setText(getString(R.string.tags))); + binding.searchTabLayout.addTab(binding.searchTabLayout.newTab().setText(getString(R.string.accounts))); + binding.searchTabLayout.addTab(binding.searchTabLayout.newTab().setText(getString(R.string.toots))); + binding.searchTabLayout.addTab(binding.searchTabLayout.newTab().setText(getString(R.string.action_cache))); + binding.searchTabLayout.setTabTextColors(ThemeHelper.getAttColor(SearchResultTabActivity.this, R.attr.mTextColor), ContextCompat.getColor(SearchResultTabActivity.this, R.color.cyanea_accent_dark_reference)); + binding.searchTabLayout.setTabIconTint(ThemeHelper.getColorStateList(SearchResultTabActivity.this)); + + binding.searchTabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() { + @Override + public void onTabSelected(TabLayout.Tab tab) { + binding.searchViewpager.setCurrentItem(tab.getPosition()); + } + + @Override + public void onTabUnselected(TabLayout.Tab tab) { + + } + + @Override + public void onTabReselected(TabLayout.Tab tab) { + Fragment fragment; + if (binding.searchViewpager.getAdapter() != null) { + fragment = (Fragment) binding.searchViewpager.getAdapter().instantiateItem(binding.searchViewpager, tab.getPosition()); + if (fragment instanceof FragmentMastodonAccount) { + FragmentMastodonAccount fragmentMastodonAccount = ((FragmentMastodonAccount) fragment); + fragmentMastodonAccount.scrollToTop(); + } else if (fragment instanceof FragmentMastodonTimeline) { + FragmentMastodonTimeline fragmentMastodonTimeline = ((FragmentMastodonTimeline) fragment); + fragmentMastodonTimeline.scrollToTop(); + } else if (fragment instanceof FragmentMastodonTag) { + FragmentMastodonTag fragmentMastodonTag = ((FragmentMastodonTag) fragment); + fragmentMastodonTag.scrollToTop(); + } + } + } + }); + } + + + @Override + public boolean onCreateOptionsMenu(@NonNull Menu menu) { + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.menu_search, menu); + SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE); + SearchView searchView = (SearchView) menu.findItem(R.id.action_search).getActionView(); + searchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName())); + searchView.setIconifiedByDefault(false); + + + searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String query) { + InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE); + assert imm != null; + imm.hideSoftInputFromWindow(binding.searchTabLayout.getWindowToken(), 0); + query = query.replaceAll("^#+", ""); + search = query.trim(); + PagerAdapter mPagerAdapter = new ScreenSlidePagerAdapter(getSupportFragmentManager()); + binding.searchViewpager.setAdapter(mPagerAdapter); + searchView.clearFocus(); + setTitle(search); + searchView.setIconified(true); + return false; + } + + @Override + public boolean onQueryTextChange(String newText) { + return false; + } + }); + + searchView.setOnCloseListener(() -> { + setTitle(search); + return false; + }); + searchView.setOnSearchClickListener(v -> { + searchView.setQuery(search, false); + searchView.setIconified(false); + }); + + + PagerAdapter mPagerAdapter = new ScreenSlidePagerAdapter(getSupportFragmentManager()); + binding.searchViewpager.setAdapter(mPagerAdapter); + + binding.searchViewpager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() { + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + } + + @Override + public void onPageSelected(int position) { + TabLayout.Tab tab = binding.searchTabLayout.getTabAt(position); + if (tab != null) + tab.select(); + } + + @Override + public void onPageScrollStateChanged(int state) { + } + }); + return true; + } + + @Override + public boolean onOptionsItemSelected(@NotNull MenuItem item) { + if (item.getItemId() == android.R.id.home) { + finish(); + return true; + } else if (item.getItemId() == R.id.action_search) { + + return true; + } + return super.onOptionsItemSelected(item); + } + + + /** + * Pager adapter for the 4 fragments + */ + private class ScreenSlidePagerAdapter extends FragmentStatePagerAdapter { + + ScreenSlidePagerAdapter(FragmentManager fm) { + super(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT); + } + + @NotNull + @Override + public Fragment getItem(int position) { + Bundle bundle = new Bundle(); + switch (position) { + case 0: + FragmentMastodonTag fragmentMastodonTag = new FragmentMastodonTag(); + bundle.putString(Helper.ARG_SEARCH_KEYWORD, search); + fragmentMastodonTag.setArguments(bundle); + return fragmentMastodonTag; + case 1: + FragmentMastodonAccount fragmentMastodonAccount = new FragmentMastodonAccount(); + bundle.putString(Helper.ARG_SEARCH_KEYWORD, search); + fragmentMastodonAccount.setArguments(bundle); + return fragmentMastodonAccount; + case 2: + FragmentMastodonTimeline fragmentMastodonTimeline = new FragmentMastodonTimeline(); + bundle.putString(Helper.ARG_SEARCH_KEYWORD, search); + fragmentMastodonTimeline.setArguments(bundle); + return fragmentMastodonTimeline; + default: + fragmentMastodonTimeline = new FragmentMastodonTimeline(); + bundle.putString(Helper.ARG_SEARCH_KEYWORD_CACHE, search); + fragmentMastodonTimeline.setArguments(bundle); + return fragmentMastodonTimeline; + } + } + + @Override + public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) { + + } + + @Override + public int getCount() { + return 4; + } + } +} diff --git a/app/src/main/java/app/fedilab/android/activities/SettingsActivity.java b/app/src/main/java/app/fedilab/android/activities/SettingsActivity.java new file mode 100644 index 000000000..2527a4f26 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/activities/SettingsActivity.java @@ -0,0 +1,203 @@ +package app.fedilab.android.activities; +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import android.os.Bundle; +import android.view.MenuItem; + +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentTransaction; + +import com.google.gson.annotations.SerializedName; + +import java.util.Locale; + +import app.fedilab.android.R; +import app.fedilab.android.databinding.ActivitySettingsBinding; +import app.fedilab.android.helper.ThemeHelper; +import app.fedilab.android.ui.fragment.settings.FragmentAdministrationSettings; +import app.fedilab.android.ui.fragment.settings.FragmentComposeSettings; +import app.fedilab.android.ui.fragment.settings.FragmentInterfaceSettings; +import app.fedilab.android.ui.fragment.settings.FragmentLanguageSettings; +import app.fedilab.android.ui.fragment.settings.FragmentNotificationsSettings; +import app.fedilab.android.ui.fragment.settings.FragmentPrivacySettings; +import app.fedilab.android.ui.fragment.settings.FragmentThemingSettings; +import app.fedilab.android.ui.fragment.settings.FragmentTimelinesSettings; + + +public class SettingsActivity extends BaseActivity { + + private ActivitySettingsBinding binding; + private boolean canGoBack; + private Fragment currentFragment; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + ThemeHelper.applyThemeBar(this); + binding = ActivitySettingsBinding.inflate(getLayoutInflater()); + setContentView(binding.getRoot()); + + if (getSupportActionBar() != null) + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + canGoBack = false; + + binding.setTimelines.setOnClickListener(v -> displaySettings(SettingsEnum.TIMELINES)); + binding.setNotifications.setOnClickListener(v -> displaySettings(SettingsEnum.NOTIFICATIONS)); + binding.setInterface.setOnClickListener(v -> displaySettings(SettingsEnum.INTERFACE)); + binding.setCompose.setOnClickListener(v -> displaySettings(SettingsEnum.COMPOSE)); + binding.setPrivacy.setOnClickListener(v -> displaySettings(SettingsEnum.PRIVACY)); + binding.setTheming.setOnClickListener(v -> displaySettings(SettingsEnum.THEMING)); + binding.setAdministration.setOnClickListener(v -> displaySettings(SettingsEnum.ADMINISTRATION)); + binding.setLanguage.setOnClickListener(v -> displaySettings(SettingsEnum.LANGUAGE)); + } + + public void displaySettings(SettingsEnum settingsEnum) { + + ThemeHelper.slideViewsToLeft(binding.buttonContainer, binding.fragmentContainer, () -> { + FragmentManager fragmentManager = getSupportFragmentManager(); + FragmentTransaction fragmentTransaction = + fragmentManager.beginTransaction(); + String category = ""; + switch (settingsEnum) { + case TIMELINES: + FragmentTimelinesSettings fragmentTimelinesSettings = new FragmentTimelinesSettings(); + fragmentTransaction.replace(R.id.fragment_container, fragmentTimelinesSettings); + currentFragment = fragmentTimelinesSettings; + category = getString(R.string.settings_category_label_timelines); + break; + case NOTIFICATIONS: + FragmentNotificationsSettings fragmentNotificationsSettings = new FragmentNotificationsSettings(); + fragmentTransaction.replace(R.id.fragment_container, fragmentNotificationsSettings); + currentFragment = fragmentNotificationsSettings; + category = getString(R.string.notifications); + break; + case INTERFACE: + FragmentInterfaceSettings fragmentInterfaceSettings = new FragmentInterfaceSettings(); + fragmentTransaction.replace(R.id.fragment_container, fragmentInterfaceSettings); + currentFragment = fragmentInterfaceSettings; + category = getString(R.string.settings_category_label_interface); + break; + case COMPOSE: + FragmentComposeSettings fragmentComposeSettings = new FragmentComposeSettings(); + fragmentTransaction.replace(R.id.fragment_container, fragmentComposeSettings); + currentFragment = fragmentComposeSettings; + category = getString(R.string.compose); + break; + case PRIVACY: + FragmentPrivacySettings fragmentPrivacySettings = new FragmentPrivacySettings(); + fragmentTransaction.replace(R.id.fragment_container, fragmentPrivacySettings); + currentFragment = fragmentPrivacySettings; + category = getString(R.string.action_privacy); + break; + case THEMING: + FragmentThemingSettings fragmentThemingSettings = new FragmentThemingSettings(); + fragmentTransaction.replace(R.id.fragment_container, fragmentThemingSettings); + currentFragment = fragmentThemingSettings; + category = getString(R.string.theming); + break; + case ADMINISTRATION: + FragmentAdministrationSettings fragmentAdministrationSettings = new FragmentAdministrationSettings(); + fragmentTransaction.replace(R.id.fragment_container, fragmentAdministrationSettings); + currentFragment = fragmentAdministrationSettings; + category = getString(R.string.administration); + break; + case LANGUAGE: + FragmentLanguageSettings fragmentLanguageSettings = new FragmentLanguageSettings(); + fragmentTransaction.replace(R.id.fragment_container, fragmentLanguageSettings); + currentFragment = fragmentLanguageSettings; + category = getString(R.string.languages); + break; + + } + String title = String.format(Locale.getDefault(), "%s - %s", getString(R.string.settings), category); + setTitle(title); + canGoBack = true; + fragmentTransaction.commit(); + }); + } + + + @Override + public void onBackPressed() { + if (canGoBack) { + canGoBack = false; + ThemeHelper.slideViewsToRight(binding.fragmentContainer, binding.buttonContainer, () -> { + if (currentFragment != null) { + FragmentManager fragmentManager = getSupportFragmentManager(); + FragmentTransaction fragmentTransaction = + fragmentManager.beginTransaction(); + fragmentTransaction.remove(currentFragment).commit(); + } + }); + setTitle(R.string.settings); + } else { + super.onBackPressed(); + } + + } + + @Override + protected void onDestroy() { + super.onDestroy(); + if (currentFragment != null) { + currentFragment.onDestroy(); + } + binding = null; + + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == android.R.id.home) { + onBackPressed(); + return true; + } + return super.onOptionsItemSelected(item); + } + + + public enum SettingsEnum { + @SerializedName("TIMELINES") + TIMELINES("TIMELINES"), + @SerializedName("NOTIFICATIONS") + NOTIFICATIONS("NOTIFICATIONS"), + @SerializedName("INTERFACE") + INTERFACE("INTERFACE"), + @SerializedName("COMPOSE") + COMPOSE("COMPOSE"), + @SerializedName("PRIVACY") + PRIVACY("PRIVACY"), + @SerializedName("THEMING") + THEMING("THEMING"), + @SerializedName("ADMINISTRATION") + ADMINISTRATION("ADMINISTRATION"), + @SerializedName("LANGUAGE") + LANGUAGE("LANGUAGE"); + + private final String value; + + SettingsEnum(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + } + +} diff --git a/app/src/main/java/app/fedilab/android/activities/StatusInfoActivity.java b/app/src/main/java/app/fedilab/android/activities/StatusInfoActivity.java new file mode 100644 index 000000000..21c335cb1 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/activities/StatusInfoActivity.java @@ -0,0 +1,140 @@ +package app.fedilab.android.activities; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + + +import android.os.Bundle; +import android.view.MenuItem; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.lifecycle.ViewModelProvider; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.ArrayList; +import java.util.List; + +import app.fedilab.android.BaseMainActivity; +import app.fedilab.android.R; +import app.fedilab.android.client.mastodon.entities.Account; +import app.fedilab.android.client.mastodon.entities.Accounts; +import app.fedilab.android.client.mastodon.entities.Status; +import app.fedilab.android.databinding.ActivityStatusInfoBinding; +import app.fedilab.android.helper.Helper; +import app.fedilab.android.helper.ThemeHelper; +import app.fedilab.android.ui.drawer.AccountAdapter; +import app.fedilab.android.viewmodel.mastodon.StatusesVM; + + +public class StatusInfoActivity extends BaseActivity { + + private ActivityStatusInfoBinding binding; + private List accountList; + private AccountAdapter accountAdapter; + private String max_id; + private typeOfInfo type; + private boolean flagLoading; + private Status status; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + ThemeHelper.applyTheme(this); + binding = ActivityStatusInfoBinding.inflate(getLayoutInflater()); + setContentView(binding.getRoot()); + setSupportActionBar(binding.toolbar); + if (getSupportActionBar() != null) { + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + getSupportActionBar().setDisplayShowHomeEnabled(true); + } + accountList = new ArrayList<>(); + Bundle b = getIntent().getExtras(); + if (b != null) { + type = (typeOfInfo) b.getSerializable(Helper.ARG_TYPE_OF_INFO); + status = (Status) b.getSerializable(Helper.ARG_STATUS); + } + if (type == null || status == null) { + finish(); + return; + } + flagLoading = false; + max_id = null; + binding.title.setText(type == typeOfInfo.BOOSTED_BY ? R.string.boosted_by : R.string.favourited_by); + StatusesVM statusesVM = new ViewModelProvider(StatusInfoActivity.this).get(StatusesVM.class); + accountAdapter = new AccountAdapter(accountList); + LinearLayoutManager mLayoutManager = new LinearLayoutManager(StatusInfoActivity.this); + binding.lvAccounts.setLayoutManager(mLayoutManager); + binding.lvAccounts.setAdapter(accountAdapter); + + binding.lvAccounts.addOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { + int firstVisibleItem = mLayoutManager.findFirstVisibleItemPosition(); + if (dy > 0) { + int visibleItemCount = mLayoutManager.getChildCount(); + int totalItemCount = mLayoutManager.getItemCount(); + if (firstVisibleItem + visibleItemCount == totalItemCount) { + if (!flagLoading) { + flagLoading = true; + binding.loadingNextAccounts.setVisibility(View.VISIBLE); + if (type == typeOfInfo.BOOSTED_BY) { + statusesVM.rebloggedBy(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, status.id, max_id, null, null).observe(StatusInfoActivity.this, accounts -> manageView(accounts)); + } else if (type == typeOfInfo.LIKED_BY) { + statusesVM.favouritedBy(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, status.id, max_id, null, null).observe(StatusInfoActivity.this, accounts -> manageView(accounts)); + } + } + } else { + binding.loadingNextAccounts.setVisibility(View.GONE); + } + } + } + }); + if (type == typeOfInfo.BOOSTED_BY) { + statusesVM.rebloggedBy(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, status.id, null, null, null).observe(StatusInfoActivity.this, this::manageView); + } else if (type == typeOfInfo.LIKED_BY) { + statusesVM.favouritedBy(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, status.id, null, null, null).observe(StatusInfoActivity.this, this::manageView); + } + } + + private void manageView(Accounts accounts) { + flagLoading = false; + binding.loadingNextAccounts.setVisibility(View.GONE); + if (accountList != null && accounts != null && accounts.accounts != null) { + int startId = 0; + //There are some statuses present in the timeline + if (accountList.size() > 0) { + startId = accountList.size(); + } + accountList.addAll(accounts.accounts); + max_id = accounts.pagination.min_id; + accountAdapter.notifyItemRangeInserted(startId, accounts.accounts.size()); + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == android.R.id.home) { + finish(); + return true; + } + return true; + } + + public enum typeOfInfo { + LIKED_BY, + BOOSTED_BY + } +} diff --git a/app/src/main/java/app/fedilab/android/activities/WebviewActivity.java b/app/src/main/java/app/fedilab/android/activities/WebviewActivity.java new file mode 100644 index 000000000..33d74a8c8 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/activities/WebviewActivity.java @@ -0,0 +1,257 @@ +package app.fedilab.android.activities; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import android.Manifest; +import android.annotation.SuppressLint; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.database.sqlite.SQLiteDatabase; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.WindowManager; +import android.widget.ArrayAdapter; +import android.widget.Toast; + +import androidx.appcompat.app.AlertDialog; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; + +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; + +import app.fedilab.android.R; +import app.fedilab.android.databinding.ActivityWebviewBinding; +import app.fedilab.android.helper.Helper; +import app.fedilab.android.helper.ThemeHelper; +import app.fedilab.android.sqlite.Sqlite; +import app.fedilab.android.webview.CustomWebview; +import app.fedilab.android.webview.FedilabWebChromeClient; +import app.fedilab.android.webview.FedilabWebViewClient; +import es.dmoral.toasty.Toasty; + + +public class WebviewActivity extends BaseActivity { + + public static List trackingDomains; + private String url; + private String peertubeLinkToFetch; + private boolean peertubeLink; + private CustomWebview webView; + private Menu defaultMenu; + private FedilabWebViewClient FedilabWebViewClient; + private ActivityWebviewBinding binding; + + @SuppressLint("SetJavaScriptEnabled") + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + ThemeHelper.applyTheme(this); + binding = ActivityWebviewBinding.inflate(getLayoutInflater()); + View view = binding.getRoot(); + setContentView(view); + + Bundle b = getIntent().getExtras(); + if (b != null) { + url = b.getString("url", null); + peertubeLinkToFetch = b.getString("peertubeLinkToFetch", null); + peertubeLink = b.getBoolean("peertubeLink", false); + } + if (url == null) + finish(); + if (getSupportActionBar() != null) + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + + webView = Helper.initializeWebview(WebviewActivity.this, R.id.webview, null); + setTitle(""); + + webView.getSettings().setJavaScriptEnabled(true); + + + FedilabWebChromeClient FedilabWebChromeClient = new FedilabWebChromeClient(WebviewActivity.this, webView, binding.webviewContainer, binding.videoLayout); + FedilabWebChromeClient.setOnToggledFullscreen(fullscreen -> { + + if (fullscreen) { + binding.videoLayout.setVisibility(View.VISIBLE); + WindowManager.LayoutParams attrs = getWindow().getAttributes(); + attrs.flags |= WindowManager.LayoutParams.FLAG_FULLSCREEN; + attrs.flags |= WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON; + getWindow().setAttributes(attrs); + getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE); + } else { + WindowManager.LayoutParams attrs = getWindow().getAttributes(); + attrs.flags &= ~WindowManager.LayoutParams.FLAG_FULLSCREEN; + attrs.flags &= ~WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON; + getWindow().setAttributes(attrs); + getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE); + binding.videoLayout.setVisibility(View.GONE); + } + }); + webView.setWebChromeClient(FedilabWebChromeClient); + FedilabWebViewClient = new FedilabWebViewClient(WebviewActivity.this); + webView.setWebViewClient(FedilabWebViewClient); + webView.setDownloadListener((url, userAgent, contentDisposition, mimetype, contentLength) -> { + + if (Build.VERSION.SDK_INT >= 23) { + if (ContextCompat.checkSelfPermission(WebviewActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED || ContextCompat.checkSelfPermission(WebviewActivity.this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions(WebviewActivity.this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE}, Helper.EXTERNAL_STORAGE_REQUEST_CODE); + } else { + Helper.manageDownloads(WebviewActivity.this, url); + } + } else { + Helper.manageDownloads(WebviewActivity.this, url); + } + }); + if (!url.toLowerCase().startsWith("http://") && !url.toLowerCase().startsWith("https://")) + url = "http://" + url; + if (trackingDomains == null) { + AsyncTask.execute(() -> { + SQLiteDatabase db = Sqlite.getInstance(getApplicationContext(), Sqlite.DB_NAME, null, Sqlite.DB_VERSION).open(); + // trackingDomains = new DomainBlockDAO(WebviewActivity.this, db).getAll(); + if (trackingDomains == null) + trackingDomains = new ArrayList<>(); + // Get a handler that can be used to post to the main thread + Handler mainHandler = new Handler(getMainLooper()); + Runnable myRunnable = () -> webView.loadUrl(url); + mainHandler.post(myRunnable); + + }); + } else + webView.loadUrl(url); + } + + + /* public void setCount(Context context, String count) { + if (defaultMenu != null && !peertubeLink) { + MenuItem menuItem = defaultMenu.findItem(R.id.action_block); + LayerDrawable icon = (LayerDrawable) menuItem.getIcon(); + + CountDrawable badge; + + // Reuse drawable if possible + Drawable reuse = icon.findDrawableByLayerId(R.id.ic_block_count); + if (reuse instanceof CountDrawable) { + badge = (CountDrawable) reuse; + } else { + badge = new CountDrawable(context); + } + + badge.setCount(count); + icon.mutate(); + icon.setDrawableByLayerId(R.id.ic_block_count, badge); + } + }*/ + + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + /* if (!peertubeLink) + setCount(WebviewActivity.this, "0");*/ + defaultMenu = menu; + return super.onPrepareOptionsMenu(menu); + } + + @Override + public boolean onCreateOptionsMenu(@NotNull Menu menu) { + getMenuInflater().inflate(R.menu.main_webview, menu); + defaultMenu = menu; + if (peertubeLink) { + menu.findItem(R.id.action_go).setVisible(false); + menu.findItem(R.id.action_block).setVisible(false); + } + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + int itemId = item.getItemId(); + if (itemId == android.R.id.home) { + finish(); + return true; + } else if (itemId == R.id.action_block) { + + + List domains = FedilabWebViewClient.getDomains(); + + final ArrayAdapter arrayAdapter = new ArrayAdapter<>(WebviewActivity.this, R.layout.domains_blocked); + arrayAdapter.addAll(domains); + + AlertDialog.Builder builder = new AlertDialog.Builder(WebviewActivity.this, Helper.dialogStyle()); + builder.setTitle(R.string.list_of_blocked_domains); + + builder.setNegativeButton(R.string.close, (dialog, which) -> dialog.dismiss()); + + builder.setAdapter(arrayAdapter, (dialog, which) -> { + String strName = arrayAdapter.getItem(which); + assert strName != null; + Toasty.info(WebviewActivity.this, strName, Toast.LENGTH_LONG).show(); + }); + builder.show(); + + return true; + } else if (itemId == R.id.action_go) { + Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); + try { + startActivity(browserIntent); + } catch (Exception e) { + Toasty.error(WebviewActivity.this, getString(R.string.toast_error), Toast.LENGTH_LONG).show(); + } + return true; + } + return super.onOptionsItemSelected(item); + } + + public void setUrl(String newUrl) { + this.url = newUrl; + } + + @Override + public void onPause() { + super.onPause(); + if (webView != null) + webView.onPause(); + } + + @Override + public void onResume() { + super.onResume(); + if (webView != null) + webView.onResume(); + } + + @Override + public void onBackPressed() { + if (webView.canGoBack()) { + webView.goBack(); + } else { + super.onBackPressed(); + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (webView != null) + webView.destroy(); + } + +} diff --git a/app/src/main/java/app/fedilab/android/activities/WebviewConnectActivity.java b/app/src/main/java/app/fedilab/android/activities/WebviewConnectActivity.java new file mode 100644 index 000000000..0141ea235 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/activities/WebviewConnectActivity.java @@ -0,0 +1,264 @@ +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +package app.fedilab.android.activities; + + +import static app.fedilab.android.BaseMainActivity.api; +import static app.fedilab.android.BaseMainActivity.currentInstance; +import static app.fedilab.android.BaseMainActivity.software; +import static app.fedilab.android.helper.Helper.PREF_USER_TOKEN; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.graphics.drawable.ColorDrawable; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.webkit.CookieManager; +import android.webkit.CookieSyncManager; +import android.webkit.WebChromeClient; +import android.webkit.WebView; +import android.webkit.WebViewClient; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.ProgressBar; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AlertDialog; +import androidx.core.content.ContextCompat; +import androidx.lifecycle.ViewModelProvider; +import androidx.preference.PreferenceManager; + +import app.fedilab.android.BaseMainActivity; +import app.fedilab.android.R; +import app.fedilab.android.client.entities.Account; +import app.fedilab.android.databinding.ActivityWebviewConnectBinding; +import app.fedilab.android.exception.DBException; +import app.fedilab.android.helper.Helper; +import app.fedilab.android.helper.ThemeHelper; +import app.fedilab.android.viewmodel.mastodon.AccountsVM; +import app.fedilab.android.viewmodel.mastodon.OauthVM; +import es.dmoral.toasty.Toasty; + + +public class WebviewConnectActivity extends BaseActivity { + + + private ActivityWebviewConnectBinding binding; + private AlertDialog alert; + private String login_url; + + @SuppressWarnings("deprecation") + public static void clearCookies(Context context) { + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) { + CookieManager.getInstance().removeAllCookies(null); + CookieManager.getInstance().flush(); + } else { + CookieSyncManager cookieSyncMngr = CookieSyncManager.createInstance(context); + cookieSyncMngr.startSync(); + CookieManager cookieManager = CookieManager.getInstance(); + cookieManager.removeAllCookie(); + cookieManager.removeSessionCookie(); + cookieSyncMngr.stopSync(); + cookieSyncMngr.sync(); + } + } + + @SuppressLint("SetJavaScriptEnabled") + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + ThemeHelper.applyTheme(this); + SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(WebviewConnectActivity.this); + + binding = ActivityWebviewConnectBinding.inflate(getLayoutInflater()); + View rootView = binding.getRoot(); + setContentView(rootView); + + Bundle b = getIntent().getExtras(); + if (b != null) { + login_url = b.getString("login_url"); + } + if (login_url == null) + finish(); + ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + LayoutInflater inflater = (LayoutInflater) this.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + assert inflater != null; + View view = inflater.inflate(R.layout.simple_bar, new LinearLayout(WebviewConnectActivity.this), false); + view.setBackground(new ColorDrawable(ContextCompat.getColor(WebviewConnectActivity.this, R.color.cyanea_primary))); + actionBar.setCustomView(view, new ActionBar.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM); + ImageView toolbar_close = actionBar.getCustomView().findViewById(R.id.toolbar_close); + TextView toolbar_title = actionBar.getCustomView().findViewById(R.id.toolbar_title); + toolbar_close.setOnClickListener(v -> finish()); + toolbar_title.setText(R.string.add_account); + } + + clearCookies(WebviewConnectActivity.this); + binding.webviewConnect.getSettings().setJavaScriptEnabled(true); + String user_agent = sharedpreferences.getString(getString(R.string.SET_CUSTOM_USER_AGENT), Helper.USER_AGENT); + binding.webviewConnect.getSettings().setUserAgentString(user_agent); + CookieManager.getInstance().setAcceptThirdPartyCookies(binding.webviewConnect, true); + + + final ProgressBar pbar = findViewById(R.id.progress_bar); + binding.webviewConnect.setWebChromeClient(new WebChromeClient() { + @Override + public void onProgressChanged(WebView view, int progress) { + if (progress < 100 && pbar.getVisibility() == ProgressBar.GONE) { + pbar.setVisibility(ProgressBar.VISIBLE); + } + pbar.setProgress(progress); + if (progress == 100) { + pbar.setVisibility(ProgressBar.GONE); + } + } + }); + + binding.webviewConnect.setWebViewClient(new WebViewClient() { + + /* @Override + public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) { + String x_xsrf_token = null; + String x_csrf_token = null; + if (request.getUrl().toString().contains("accounts/verify_credentials")) { + + String cookies = CookieManager.getInstance().getCookie(request.getUrl().toString()); + + Map requestHeaders = request.getRequestHeaders(); + Iterator> it = requestHeaders.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry pair = it.next(); + if (pair.getKey().compareTo("X-XSRF-TOKEN") == 0) { + x_xsrf_token = pair.getValue(); + } + if (pair.getKey().compareTo("X-CSRF-TOKEN") == 0) { + x_csrf_token = pair.getValue(); + } + it.remove(); + } + if (x_xsrf_token != null && x_csrf_token != null) { + String finalX_xsrf_token = x_xsrf_token; + String finalX_csrf_token = x_csrf_token; + new Handler(Looper.getMainLooper()).post(() -> { + view.stopLoading(); + SharedPreferences sharedpreferences1 = getSharedPreferences(Helper.APP_PREFS, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = sharedpreferences1.edit(); + String token = "X-XSRF-TOKEN= " + finalX_xsrf_token + ";X-CSRF-TOKEN= " + finalX_csrf_token + "|" + cookies; + editor.putString(Helper.PREF_KEY_OAUTH_TOKEN, token); + editor.commit(); + view.setVisibility(View.GONE); + //Update the account with the token; + new UpdateAccountInfoAsyncTask(WebviewConnectActivity.this, token, clientId, clientSecret, null, instance, social); + }); + } + } + return super.shouldInterceptRequest(view, request); + }*/ + + + @Override + public boolean shouldOverrideUrlLoading(WebView view, String url) { + super.shouldOverrideUrlLoading(view, url); + if (url.contains(Helper.REDIRECT_CONTENT_WEB)) { + String[] val = url.split("code="); + if (val.length < 2) { + Toasty.error(WebviewConnectActivity.this, getString(R.string.toast_code_error), Toast.LENGTH_LONG).show(); + Intent myIntent = new Intent(WebviewConnectActivity.this, LoginActivity.class); + startActivity(myIntent); + finish(); + return false; + } + String code = val[1]; + OauthVM oauthVM = new ViewModelProvider(WebviewConnectActivity.this).get(OauthVM.class); + //API call to get the user token + oauthVM.createToken(currentInstance, "authorization_code", BaseMainActivity.client_id, BaseMainActivity.client_secret, Helper.REDIRECT_CONTENT_WEB, Helper.OAUTH_SCOPES, code) + .observe(WebviewConnectActivity.this, tokenObj -> { + Account account = new Account(); + account.client_id = BaseMainActivity.client_id; + account.client_secret = BaseMainActivity.client_secret; + account.token = tokenObj.token_type + " " + tokenObj.access_token; + account.api = api; + account.software = software; + account.instance = currentInstance; + //API call to retrieve account information for the new token + AccountsVM accountsVM = new ViewModelProvider(WebviewConnectActivity.this).get(AccountsVM.class); + accountsVM.getConnectedAccount(currentInstance, account.token).observe(WebviewConnectActivity.this, mastodonAccount -> { + account.mastodon_account = mastodonAccount; + account.user_id = mastodonAccount.id; + new Thread(() -> { + try { + //update the database + new Account(WebviewConnectActivity.this).insertOrUpdate(account); + Handler mainHandler = new Handler(Looper.getMainLooper()); + BaseMainActivity.currentToken = account.token; + BaseMainActivity.currentUserID = account.user_id; + api = Account.API.MASTODON; + SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(WebviewConnectActivity.this); + SharedPreferences.Editor editor = sharedpreferences.edit(); + editor.putString(PREF_USER_TOKEN, account.token); + editor.commit(); + //The user is now authenticated, it will be redirected to MainActivity + Runnable myRunnable = () -> { + Intent mainActivity = new Intent(WebviewConnectActivity.this, BaseMainActivity.class); + startActivity(mainActivity); + finish(); + }; + mainHandler.post(myRunnable); + } catch (DBException e) { + e.printStackTrace(); + } + }).start(); + }); + }); + return true; + } else { + return false; + } + } + }); + + binding.webviewConnect.loadUrl(login_url); + } + + @Override + public void onBackPressed() { + if (binding.webviewConnect.canGoBack()) { + binding.webviewConnect.goBack(); + } else { + super.onBackPressed(); + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (alert != null) { + alert.dismiss(); + alert = null; + } + binding.webviewConnect.destroy(); + } +} \ No newline at end of file diff --git a/app/src/main/java/app/fedilab/android/broadcastreceiver/NetworkStateReceiver.java b/app/src/main/java/app/fedilab/android/broadcastreceiver/NetworkStateReceiver.java new file mode 100644 index 000000000..20185de2b --- /dev/null +++ b/app/src/main/java/app/fedilab/android/broadcastreceiver/NetworkStateReceiver.java @@ -0,0 +1,83 @@ +package app.fedilab.android.broadcastreceiver; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; + +import java.util.HashSet; +import java.util.Set; + + +/** + * Original work from https://stackoverflow.com/a/25873554 + */ +public class NetworkStateReceiver extends BroadcastReceiver { + + protected Set listeners; + protected Boolean connected; + + public NetworkStateReceiver() { + listeners = new HashSet<>(); + connected = null; + } + + @Override + public void onReceive(Context context, Intent intent) { + if (intent == null || intent.getExtras() == null) + return; + ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo ni = manager.getActiveNetworkInfo(); + if (ni != null && ni.getState() == NetworkInfo.State.CONNECTED) { + connected = true; + } else if (intent.getBooleanExtra(ConnectivityManager.EXTRA_NO_CONNECTIVITY, Boolean.FALSE)) { + connected = false; + } + notifyStateToAll(); + } + + + private void notifyStateToAll() { + for (NetworkStateReceiverListener listener : listeners) + notifyState(listener); + } + + private void notifyState(NetworkStateReceiverListener listener) { + if (connected == null || listener == null) + return; + if (connected) + listener.networkAvailable(); + else + listener.networkUnavailable(); + } + + public void addListener(NetworkStateReceiverListener l) { + listeners.add(l); + notifyState(l); + } + + public void removeListener(NetworkStateReceiverListener l) { + listeners.remove(l); + } + + public interface NetworkStateReceiverListener { + void networkAvailable(); + + void networkUnavailable(); + } +} \ No newline at end of file diff --git a/app/src/main/java/app/fedilab/android/broadcastreceiver/ToastMessage.java b/app/src/main/java/app/fedilab/android/broadcastreceiver/ToastMessage.java new file mode 100644 index 000000000..65e67352d --- /dev/null +++ b/app/src/main/java/app/fedilab/android/broadcastreceiver/ToastMessage.java @@ -0,0 +1,53 @@ +package app.fedilab.android.broadcastreceiver; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import static app.fedilab.android.helper.Helper.RECEIVE_TOAST_CONTENT; +import static app.fedilab.android.helper.Helper.RECEIVE_TOAST_TYPE; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; + +import app.fedilab.android.helper.Helper; +import es.dmoral.toasty.Toasty; + +public class ToastMessage extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + Bundle b = intent.getExtras(); + if (b != null) { + String type = b.getString(RECEIVE_TOAST_TYPE, null); + String content = b.getString(RECEIVE_TOAST_CONTENT, null); + if (type != null && content != null) { + switch (type) { + case Helper.RECEIVE_TOAST_TYPE_ERROR: + Toasty.error(context, content, Toasty.LENGTH_SHORT).show(); + break; + case Helper.RECEIVE_TOAST_TYPE_WARNING: + Toasty.warning(context, content, Toasty.LENGTH_SHORT).show(); + break; + case Helper.RECEIVE_TOAST_TYPE_INFO: + Toasty.info(context, content, Toasty.LENGTH_SHORT).show(); + break; + case Helper.RECEIVE_TOAST_TYPE_SUCCESS: + Toasty.success(context, content, Toasty.LENGTH_SHORT).show(); + break; + } + } + } + } +} diff --git a/app/src/main/java/app/fedilab/android/client/NodeInfoService.java b/app/src/main/java/app/fedilab/android/client/NodeInfoService.java new file mode 100644 index 000000000..fc54f220e --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/NodeInfoService.java @@ -0,0 +1,33 @@ +package app.fedilab.android.client; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + + +import app.fedilab.android.client.entities.WellKnownNodeinfo; +import retrofit2.Call; +import retrofit2.http.GET; +import retrofit2.http.Path; + +public interface NodeInfoService { + + @GET(".well-known/nodeinfo") + Call getWellKnownNodeinfoLinks(); + + @GET("{nodeInfoPath}") + Call getNodeinfo( + @Path(value = "nodeInfoPath", encoded = true) String nodeInfoPath + ); + +} diff --git a/app/src/main/java/app/fedilab/android/client/entities/Account.java b/app/src/main/java/app/fedilab/android/client/entities/Account.java new file mode 100644 index 000000000..9e07bd366 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/entities/Account.java @@ -0,0 +1,441 @@ +package app.fedilab.android.client.entities; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; + +import com.google.gson.Gson; +import com.google.gson.annotations.SerializedName; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import app.fedilab.android.BaseMainActivity; +import app.fedilab.android.exception.DBException; +import app.fedilab.android.helper.Helper; +import app.fedilab.android.sqlite.Sqlite; + +/** + * Class that manages Accounts from database + * Accounts details are serialized and can be for different softwares + * The type of the software is stored in api field + */ +public class Account implements Serializable { + + + private final SQLiteDatabase db; + @SerializedName("user_id") + public String user_id; + @SerializedName("instance") + public String instance; + @SerializedName("api") + public API api; + @SerializedName("software") + public String software; + @SerializedName("token") + public String token; + @SerializedName("refresh_token") + public String refresh_token; + @SerializedName("token_validity") + public long token_validity; + @SerializedName("client_id") + public String client_id; + @SerializedName("client_secret") + public String client_secret; + @SerializedName("created_at") + public Date created_at; + @SerializedName("updated_at") + public Date updated_at; + @SerializedName("mastodon_account") + public app.fedilab.android.client.mastodon.entities.Account mastodon_account; + + private transient Context context; + + public Account() { + db = null; + } + + public Account(Context context) { + //Creation of the DB with tables + this.context = context; + this.db = Sqlite.getInstance(context.getApplicationContext(), Sqlite.DB_NAME, null, Sqlite.DB_VERSION).open(); + } + + /** + * Serialized a Mastodon Account class + * + * @param mastodon_account {@link app.fedilab.android.client.mastodon.entities.Account} to serialize + * @return String serialized account + */ + public static String mastodonAccountToStringStorage(app.fedilab.android.client.mastodon.entities.Account mastodon_account) { + Gson gson = new Gson(); + try { + return gson.toJson(mastodon_account); + } catch (Exception e) { + return null; + } + } + + /** + * Returns all Account in db + * + * @return Account List + */ + public List getPushNotificationAccounts() { + + try { + Cursor c = db.query(Sqlite.TABLE_USER_ACCOUNT, null, "(" + Sqlite.COL_API + " = 'MASTODON' OR " + Sqlite.COL_API + " = 'PLEROMA') AND " + Sqlite.COL_TOKEN + " IS NOT NULL", null, null, null, Sqlite.COL_INSTANCE + " ASC", null); + return cursorToListUserWithOwner(c); + } catch (Exception e) { + return null; + } + } + + /** + * Unserialized a Mastodon Account + * + * @param serializedAccount String serialized account + * @return {@link app.fedilab.android.client.mastodon.entities.Account} + */ + public static app.fedilab.android.client.mastodon.entities.Account restoreAccountFromString(String serializedAccount) { + Gson gson = new Gson(); + try { + return gson.fromJson(serializedAccount, app.fedilab.android.client.mastodon.entities.Account.class); + } catch (Exception e) { + return null; + } + } + + /** + * Insert or update a user + * + * @param account {@link Account} + * @return long - db id + * @throws DBException exception with database + */ + public long insertOrUpdate(Account account) throws DBException { + if (db == null) { + throw new DBException("db is null. Wrong initialization."); + } + boolean exists = accountExist(account); + long idReturned; + if (exists) { + idReturned = updateAccount(account); + } else { + idReturned = insertAccount(account); + } + return idReturned; + } + + /** + * Insert an account in db + * + * @param account {@link Account} + * @return long - db id + * @throws DBException exception with database + */ + private long insertAccount(Account account) throws DBException { + if (db == null) { + throw new DBException("db is null. Wrong initialization."); + } + ContentValues values = new ContentValues(); + values.put(Sqlite.COL_APP_CLIENT_ID, account.client_id); + values.put(Sqlite.COL_APP_CLIENT_SECRET, account.client_secret); + values.put(Sqlite.COL_USER_ID, account.user_id); + values.put(Sqlite.COL_INSTANCE, account.instance); + values.put(Sqlite.COL_API, account.api.name()); + values.put(Sqlite.COL_SOFTWARE, account.software); + values.put(Sqlite.COL_TOKEN_VALIDITY, account.token_validity); + values.put(Sqlite.COL_TOKEN, account.token); + values.put(Sqlite.COL_REFRESH_TOKEN, account.refresh_token); + if (account.mastodon_account != null) { + values.put(Sqlite.COL_ACCOUNT, mastodonAccountToStringStorage(account.mastodon_account)); + } + values.put(Sqlite.COL_CREATED_AT, Helper.dateToString(new Date())); + values.put(Sqlite.COL_UPDATED_AT, Helper.dateToString(new Date())); + //Inserts token + try { + return db.insertOrThrow(Sqlite.TABLE_USER_ACCOUNT, null, values); + } catch (Exception e) { + e.printStackTrace(); + return -1; + } + } + + /** + * Update an account in db + * + * @param account {@link Account} + * @return long - db id + * @throws DBException exception with database + */ + private long updateAccount(Account account) throws DBException { + if (db == null) { + throw new DBException("db is null. Wrong initialization."); + } + ContentValues values = new ContentValues(); + //Can be null if only retrieving account details - IE : not the whole authentication process + if (account.client_id != null) { + values.put(Sqlite.COL_APP_CLIENT_ID, account.client_id); + values.put(Sqlite.COL_APP_CLIENT_SECRET, account.client_secret); + values.put(Sqlite.COL_API, account.api.name()); + values.put(Sqlite.COL_SOFTWARE, account.software); + values.put(Sqlite.COL_TOKEN_VALIDITY, account.token_validity); + values.put(Sqlite.COL_TOKEN, account.token); + values.put(Sqlite.COL_REFRESH_TOKEN, account.refresh_token); + } + if (account.mastodon_account != null) { + values.put(Sqlite.COL_ACCOUNT, mastodonAccountToStringStorage(account.mastodon_account)); + } + values.put(Sqlite.COL_UPDATED_AT, Helper.dateToString(new Date())); + //Inserts token + try { + return db.update(Sqlite.TABLE_USER_ACCOUNT, + values, Sqlite.COL_USER_ID + " = ? AND " + Sqlite.COL_INSTANCE + " =?", + new String[]{account.user_id, account.instance}); + } catch (Exception e) { + e.printStackTrace(); + return -1; + } + } + + /** + * Check if a user exists in db + * + * @param account Account {@link Account} + * @return boolean - user exists + * @throws DBException Exception + */ + public boolean accountExist(Account account) throws DBException { + if (db == null) { + throw new DBException("db is null. Wrong initialization."); + } + Cursor mCount = db.rawQuery("select count(*) from " + Sqlite.TABLE_USER_ACCOUNT + + " where " + Sqlite.COL_USER_ID + " = '" + account.user_id + "' AND " + Sqlite.COL_INSTANCE + " = '" + account.instance + "'", null); + mCount.moveToFirst(); + int count = mCount.getInt(0); + mCount.close(); + return (count > 0); + } + + /** + * Returns an Account by token + * + * @param userId String + * @param instance String + * @return Account {@link Account} + */ + public Account getUniqAccount(String userId, String instance) throws DBException { + if (db == null) { + throw new DBException("db is null. Wrong initialization."); + } + try { + Cursor c = db.query(Sqlite.TABLE_USER_ACCOUNT, null, Sqlite.COL_USER_ID + " = \"" + userId + "\" AND " + Sqlite.COL_INSTANCE + " = \"" + instance + "\"", null, null, null, null, "1"); + return cursorToUser(c); + } catch (Exception e) { + return null; + } + } + + /** + * Returns authenticated Account + * + * @return Account {@link Account} + */ + public Account getConnectedAccount() throws DBException { + if (db == null) { + throw new DBException("db is null. Wrong initialization."); + } + try { + Cursor c = db.query(Sqlite.TABLE_USER_ACCOUNT, null, Sqlite.COL_TOKEN + " = '" + BaseMainActivity.currentToken + "'", null, null, null, null, "1"); + return cursorToUser(c); + } catch (Exception e) { + return null; + } + } + + + /** + * Returns all accounts that allows cross-account actions + * + * @return Account List<{@link Account}> + */ + public List getCrossAccounts() throws DBException { + if (db == null) { + throw new DBException("db is null. Wrong initialization."); + } + try { + Cursor c = db.query(Sqlite.TABLE_USER_ACCOUNT, null, Sqlite.COL_API + " = 'MASTODON'", null, null, null, null, null); + return cursorToListUser(c); + } catch (Exception e) { + return null; + } + } + + /** + * Returns all accounts + * + * @return Account List<{@link Account}> + */ + public List getAll() throws DBException { + if (db == null) { + throw new DBException("db is null. Wrong initialization."); + } + try { + Cursor c = db.query(Sqlite.TABLE_USER_ACCOUNT, null, null, null, null, null, null, null); + return cursorToListUser(c); + } catch (Exception e) { + return null; + } + } + + /** + * Returns last used account + * + * @return Account {@link Account} + */ + public Account getLastUsedAccount() throws DBException { + if (db == null) { + throw new DBException("db is null. Wrong initialization."); + } + try { + Cursor c = db.query(Sqlite.TABLE_USER_ACCOUNT, null, null, null, null, null, Sqlite.COL_UPDATED_AT + " DESC", "1"); + return cursorToUser(c); + } catch (Exception e) { + return null; + } + } + + /** + * Remove an account from db + * + * @param account {@link Account} + * @return int + */ + public int removeUser(Account account) throws DBException { + if (db == null) { + throw new DBException("db is null. Wrong initialization."); + } + return db.delete(Sqlite.TABLE_USER_ACCOUNT, Sqlite.COL_USER_ID + " = '" + account.user_id + + "' AND " + Sqlite.COL_INSTANCE + " = '" + account.instance + "'", null); + } + + + private List cursorToListUser(Cursor c) { + //No element found + if (c.getCount() == 0) { + c.close(); + return null; + } + List accountList = new ArrayList<>(); + while (c.moveToNext()) { + Account account = convertCursorToAccount(c); + //We don't add in the list the current connected account + if (!account.token.equalsIgnoreCase(BaseMainActivity.currentToken)) { + accountList.add(account); + } + } + //Close the cursor + c.close(); + return accountList; + } + + private List cursorToListUserWithOwner(Cursor c) { + //No element found + if (c.getCount() == 0) { + c.close(); + return null; + } + List accountList = new ArrayList<>(); + while (c.moveToNext()) { + Account account = convertCursorToAccount(c); + //We don't add in the list the current connected account + accountList.add(account); + } + //Close the cursor + c.close(); + return accountList; + } + + /*** + * Method to hydrate an Account from database + * @param c Cursor + * @return Account {@link Account} + */ + private Account cursorToUser(Cursor c) { + //No element found + if (c.getCount() == 0) { + c.close(); + return null; + } + //Take the first element + c.moveToFirst(); + //New user + Account account = convertCursorToAccount(c); + //Close the cursor + c.close(); + return account; + } + + /** + * Read cursor and hydrate without closing it + * + * @param c - Cursor + * @return Account + */ + private Account convertCursorToAccount(Cursor c) { + Account account = new Account(); + account.user_id = c.getString(c.getColumnIndexOrThrow(Sqlite.COL_USER_ID)); + account.client_id = c.getString(c.getColumnIndexOrThrow(Sqlite.COL_APP_CLIENT_ID)); + account.client_secret = c.getString(c.getColumnIndexOrThrow(Sqlite.COL_APP_CLIENT_SECRET)); + account.token = c.getString(c.getColumnIndexOrThrow(Sqlite.COL_TOKEN)); + account.instance = c.getString(c.getColumnIndexOrThrow(Sqlite.COL_INSTANCE)); + account.refresh_token = c.getString(c.getColumnIndexOrThrow(Sqlite.COL_REFRESH_TOKEN)); + account.token_validity = c.getInt(c.getColumnIndexOrThrow(Sqlite.COL_TOKEN_VALIDITY)); + account.created_at = Helper.stringToDate(context, c.getString(c.getColumnIndexOrThrow(Sqlite.COL_CREATED_AT))); + account.updated_at = Helper.stringToDate(context, c.getString(c.getColumnIndexOrThrow(Sqlite.COL_UPDATED_AT))); + account.software = c.getString(c.getColumnIndexOrThrow(Sqlite.COL_SOFTWARE)); + String apiStr = c.getString(c.getColumnIndexOrThrow(Sqlite.COL_API)); + API api = null; + switch (apiStr) { + case "MASTODON": + api = API.MASTODON; + break; + case "PEERTUBE": + api = API.PEERTUBE; + break; + case "PIXELFED": + api = API.PIXELFED; + break; + } + account.api = api; + if (api == API.MASTODON) { + account.mastodon_account = restoreAccountFromString(c.getString(c.getColumnIndexOrThrow(Sqlite.COL_ACCOUNT))); + } + + return account; + } + + public enum API { + MASTODON, + PEERTUBE, + PIXELFED + } +} diff --git a/app/src/main/java/app/fedilab/android/client/entities/InstanceSocial.java b/app/src/main/java/app/fedilab/android/client/entities/InstanceSocial.java new file mode 100644 index 000000000..e879cc07d --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/entities/InstanceSocial.java @@ -0,0 +1,55 @@ +package app.fedilab.android.client.entities; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import com.google.gson.annotations.SerializedName; + +import java.util.Date; +import java.util.List; + +public class InstanceSocial { + + @SerializedName("instances") + public List instances; + + public static class Instance { + @SerializedName("name") + public String name; + @SerializedName("added_at") + public Date added_at; + @SerializedName("updated_at") + public Date updated_at; + @SerializedName("checked_at") + public Date checked_at; + @SerializedName("uptime") + public float uptime; + @SerializedName("up") + public boolean up; + @SerializedName("version") + public String version; + @SerializedName("thumbnail") + public String thumbnail; + @SerializedName("dead") + public boolean dead; + @SerializedName("active_users") + public int active_users; + @SerializedName("statuses") + public int statuses; + @SerializedName("email") + public String email; + @SerializedName("admin") + public String admin; + } +} diff --git a/app/src/main/java/app/fedilab/android/client/entities/Pinned.java b/app/src/main/java/app/fedilab/android/client/entities/Pinned.java new file mode 100644 index 000000000..b0d3bcb71 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/entities/Pinned.java @@ -0,0 +1,212 @@ +package app.fedilab.android.client.entities; +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; + +import com.google.gson.Gson; +import com.google.gson.annotations.SerializedName; +import com.google.gson.reflect.TypeToken; + +import java.io.Serializable; +import java.util.Date; +import java.util.List; + +import app.fedilab.android.BaseMainActivity; +import app.fedilab.android.client.entities.app.PinnedTimeline; +import app.fedilab.android.exception.DBException; +import app.fedilab.android.helper.Helper; +import app.fedilab.android.sqlite.Sqlite; + + +public class Pinned implements Serializable { + private final SQLiteDatabase db; + + @SerializedName("id") + public long id = -1; + @SerializedName("instance") + public String instance; + @SerializedName("user_id") + public String user_id; + @SerializedName("pinnedTimelines") + public List pinnedTimelines; + @SerializedName("created_at") + public Date created_ad; + @SerializedName("updated_at") + public Date updated_at; + private Context context; + + public Pinned() { + db = null; + } + + public Pinned(Context context) { + //Creation of the DB with tables + this.context = context; + this.db = Sqlite.getInstance(context.getApplicationContext(), Sqlite.DB_NAME, null, Sqlite.DB_VERSION).open(); + } + + /** + * Serialized a list of PinnedTimeline class + * + * @param pinnedTimelines List of {@link PinnedTimeline} to serialize + * @return String serialized pinnedTimelines list + */ + public static String mastodonPinnedTimelinesToStringStorage(List pinnedTimelines) { + Gson gson = new Gson(); + try { + return gson.toJson(pinnedTimelines); + } catch (Exception e) { + return null; + } + } + + /** + * Unserialized a PinnedTimeline List + * + * @param serializedPinnedTimelines String serialized PinnedTimeline list + * @return List of {@link PinnedTimeline} + */ + public static List restorePinnedTimelinesFromString(String serializedPinnedTimelines) { + Gson gson = new Gson(); + try { + return gson.fromJson(serializedPinnedTimelines, new TypeToken>() { + }.getType()); + } catch (Exception e) { + return null; + } + } + + /** + * Insert pinnedTimeline in db + * + * @param pinned {@link Pinned} + * @return long - db id + * @throws DBException exception with database + */ + public long insertPinned(Pinned pinned) throws DBException { + if (db == null) { + throw new DBException("db is null. Wrong initialization."); + } + ContentValues values = new ContentValues(); + values.put(Sqlite.COL_INSTANCE, BaseMainActivity.currentInstance); + values.put(Sqlite.COL_USER_ID, BaseMainActivity.currentUserID); + values.put(Sqlite.COL_PINNED_TIMELINES, mastodonPinnedTimelinesToStringStorage(pinned.pinnedTimelines)); + values.put(Sqlite.COL_CREATED_AT, Helper.dateToString(new Date())); + //Inserts pinned + try { + return db.insertOrThrow(Sqlite.TABLE_PINNED_TIMELINES, null, values); + } catch (Exception e) { + e.printStackTrace(); + return -1; + } + } + + + /** + * update pinned in db + * + * @param pinned {@link Pinned} + * @return long - db id + * @throws DBException exception with database + */ + public long updatePinned(Pinned pinned) throws DBException { + if (db == null) { + throw new DBException("db is null. Wrong initialization."); + } + ContentValues values = new ContentValues(); + values.put(Sqlite.COL_PINNED_TIMELINES, mastodonPinnedTimelinesToStringStorage(pinned.pinnedTimelines)); + values.put(Sqlite.COL_UPDATED_AT, Helper.dateToString(new Date())); + //Inserts token + try { + return db.update(Sqlite.TABLE_PINNED_TIMELINES, + values, Sqlite.COL_INSTANCE + " = ? AND " + Sqlite.COL_USER_ID + " = ?", + new String[]{pinned.instance, pinned.user_id}); + } catch (Exception e) { + e.printStackTrace(); + return -1; + } + } + + /** + * Returns the pinned timeline for an account + * + * @param account Account + * @return Pinned - {@link Pinned} + */ + public Pinned getPinned(Account account) throws DBException { + if (db == null) { + throw new DBException("db is null. Wrong initialization."); + } + try { + Cursor c = db.query(Sqlite.TABLE_PINNED_TIMELINES, null, Sqlite.COL_INSTANCE + " = '" + account.instance + "' AND " + Sqlite.COL_USER_ID + " = '" + account.user_id + "'", null, null, null, Sqlite.COL_UPDATED_AT + " DESC", "1"); + return cursorToPined(c); + } catch (Exception e) { + return null; + } + } + + public boolean pinnedExist(Pinned pinned) throws DBException { + if (db == null) { + throw new DBException("db is null. Wrong initialization."); + } + Cursor mCount = db.rawQuery("select count(*) from " + Sqlite.TABLE_PINNED_TIMELINES + + " where " + Sqlite.COL_INSTANCE + " = '" + pinned.instance + "' AND " + Sqlite.COL_USER_ID + " = '" + pinned.user_id + "'", null); + mCount.moveToFirst(); + int count = mCount.getInt(0); + mCount.close(); + return (count > 0); + } + + /** + * Restore pinned from db + * + * @param c Cursor + * @return Pinned + */ + private Pinned cursorToPined(Cursor c) { + //No element found + if (c.getCount() == 0) { + c.close(); + return null; + } + //Take the first element + c.moveToFirst(); + Pinned pinned = convertCursorToStatusDraft(c); + //Close the cursor + c.close(); + return pinned; + } + + /** + * Read cursor and hydrate without closing it + * + * @param c - Cursor + * @return Timeline + */ + private Pinned convertCursorToStatusDraft(Cursor c) { + Pinned pinned = new Pinned(); + pinned.id = c.getInt(c.getColumnIndexOrThrow(Sqlite.COL_ID)); + pinned.instance = c.getString(c.getColumnIndexOrThrow(Sqlite.COL_INSTANCE)); + pinned.user_id = c.getString(c.getColumnIndexOrThrow(Sqlite.COL_USER_ID)); + pinned.pinnedTimelines = restorePinnedTimelinesFromString(c.getString(c.getColumnIndexOrThrow(Sqlite.COL_PINNED_TIMELINES))); + pinned.created_ad = Helper.stringToDate(context, c.getString(c.getColumnIndexOrThrow(Sqlite.COL_CREATED_AT))); + pinned.updated_at = Helper.stringToDate(context, c.getString(c.getColumnIndexOrThrow(Sqlite.COL_UPDATED_AT))); + return pinned; + } +} diff --git a/app/src/main/java/app/fedilab/android/client/entities/PostState.java b/app/src/main/java/app/fedilab/android/client/entities/PostState.java new file mode 100644 index 000000000..f78d26ee5 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/entities/PostState.java @@ -0,0 +1,40 @@ +package app.fedilab.android.client.entities; +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import com.google.gson.annotations.SerializedName; + +import java.io.Serializable; +import java.util.List; + +public class PostState implements Serializable { + + @SerializedName("number_of_posts") + public int number_of_posts; + @SerializedName("posts_successfully_sent") + public int posts_successfully_sent; + @SerializedName("posts") + public List posts; + + public static class Post implements Serializable { + @SerializedName("id") + public String id; + @SerializedName("in_reply_to_id") + public String in_reply_to_id; + @SerializedName("number_of_media") + public int number_of_media; + } + +} diff --git a/app/src/main/java/app/fedilab/android/client/entities/ScheduledBoost.java b/app/src/main/java/app/fedilab/android/client/entities/ScheduledBoost.java new file mode 100644 index 000000000..0b8258591 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/entities/ScheduledBoost.java @@ -0,0 +1,225 @@ +package app.fedilab.android.client.entities; +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import static app.fedilab.android.client.entities.StatusCache.mastodonStatusToStringStorage; +import static app.fedilab.android.client.entities.StatusCache.restoreStatusFromString; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; + +import com.google.gson.Gson; +import com.google.gson.annotations.SerializedName; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.UUID; + +import app.fedilab.android.BaseMainActivity; +import app.fedilab.android.client.mastodon.entities.Status; +import app.fedilab.android.exception.DBException; +import app.fedilab.android.helper.Helper; +import app.fedilab.android.sqlite.Sqlite; + +public class ScheduledBoost implements Serializable { + + private transient final SQLiteDatabase db; + @SerializedName("id") + public long id = -1; + @SerializedName("instance") + public String instance; + @SerializedName("userId") + public String userId; + @SerializedName("statusId") + public String statusId; + @SerializedName("status") + public Status status; + @SerializedName("reblogged") + public int reblogged; + @SerializedName("workerUuid") + public UUID workerUuid; + @SerializedName("scheduledAt") + public Date scheduledAt; + + private Context context; + + public ScheduledBoost() { + db = null; + } + + public ScheduledBoost(Context context) { + //Creation of the DB with tables + this.db = Sqlite.getInstance(context.getApplicationContext(), Sqlite.DB_NAME, null, Sqlite.DB_VERSION).open(); + this.context = context; + } + + /** + * Serialized a UUID + * + * @param uuid {@link UUID} to serialize + * @return String serialized uuid + */ + public static String uuidToStringStorage(UUID uuid) { + Gson gson = new Gson(); + try { + return gson.toJson(uuid); + } catch (Exception e) { + return null; + } + } + + /** + * Unserialized a UUID + * + * @param serializedUUID String serialized UUID + * @return {@link UUID} + */ + public static UUID restoreUuidFromString(String serializedUUID) { + Gson gson = new Gson(); + try { + return gson.fromJson(serializedUUID, UUID.class); + } catch (Exception e) { + return null; + } + } + + /** + * * Remove a scheduled boost + * + * @param instance - String + * @param userId - String + * @param statusId - String + * @return long + * @throws DBException exception + */ + public int removeScheduled(String instance, String userId, String statusId) throws DBException { + if (db == null) { + throw new DBException("db is null. Wrong initialization."); + } + return db.delete(Sqlite.TABLE_SCHEDULE_BOOST, Sqlite.COL_INSTANCE + " = '" + instance + "' AND " + + Sqlite.COL_USER_ID + " = '" + userId + "' AND " + + Sqlite.COL_STATUS_ID + " = '" + statusId + "'" + , null); + } + + /** + * Insert scheduled boost in db + * + * @param scheduledBoost - ScheduledBoost + * @return long - db id + * @throws DBException exception with database + */ + public long insertScheduledBoost(ScheduledBoost scheduledBoost) throws DBException { + if (db == null) { + throw new DBException("db is null. Wrong initialization."); + } + ContentValues values = new ContentValues(); + values.put(Sqlite.COL_INSTANCE, BaseMainActivity.currentInstance); + values.put(Sqlite.COL_USER_ID, BaseMainActivity.currentUserID); + values.put(Sqlite.COL_STATUS_ID, scheduledBoost.statusId); + values.put(Sqlite.COL_WORKER_UUID, uuidToStringStorage(scheduledBoost.workerUuid)); + values.put(Sqlite.COL_STATUS, mastodonStatusToStringStorage(scheduledBoost.status)); + values.put(Sqlite.COL_SCHEDULED_AT, Helper.dateToString(scheduledBoost.scheduledAt)); + values.put(Sqlite.COL_REBLOGGED, 0); + //Inserts scheduled + try { + return db.insertOrThrow(Sqlite.TABLE_SCHEDULE_BOOST, null, values); + } catch (Exception e) { + e.printStackTrace(); + return -1; + } + } + + /** + * * Remove a scheduled boost + * + * @param scheduledBoost - ScheduledBoost + * @return long + * @throws DBException exception + */ + public int removeScheduled(ScheduledBoost scheduledBoost) throws DBException { + if (db == null) { + throw new DBException("db is null. Wrong initialization."); + } + return db.delete(Sqlite.TABLE_SCHEDULE_BOOST, Sqlite.COL_INSTANCE + " = '" + scheduledBoost.instance + "' AND " + + Sqlite.COL_USER_ID + " = '" + scheduledBoost.userId + "' AND " + + Sqlite.COL_STATUS_ID + " = '" + scheduledBoost.statusId + "'" + , null); + } + + /** + * Returns the ScheduledBoost for an account that has been scheduled by the client + * + * @param account Account + * @return List - List of {@link ScheduledBoost} + */ + public List getScheduled(Account account) throws DBException { + if (db == null) { + throw new DBException("db is null. Wrong initialization."); + } + try { + Cursor c = db.query(Sqlite.TABLE_SCHEDULE_BOOST, null, Sqlite.COL_INSTANCE + " = '" + account.instance + "' AND " + Sqlite.COL_USER_ID + " = '" + account.user_id + "' AND " + Sqlite.COL_WORKER_UUID + " != ''", null, null, null, Sqlite.COL_ID + " DESC", null); + return cursorToScheduledBoost(c); + } catch (Exception e) { + return null; + } + } + + /** + * Restore scheduledBoost list from db + * + * @param c Cursor + * @return List + */ + private List cursorToScheduledBoost(Cursor c) { + //No element found + if (c.getCount() == 0) { + c.close(); + return null; + } + List scheduledBoosts = new ArrayList<>(); + while (c.moveToNext()) { + ScheduledBoost scheduledBoost = convertCursorToScheduledBoost(c); + scheduledBoosts.add(scheduledBoost); + } + //Close the cursor + c.close(); + return scheduledBoosts; + } + + /** + * Read cursor and hydrate without closing it + * + * @param c - Cursor + * @return Timeline + */ + private ScheduledBoost convertCursorToScheduledBoost(Cursor c) { + ScheduledBoost scheduledBoost = new ScheduledBoost(); + scheduledBoost.id = c.getInt(c.getColumnIndexOrThrow(Sqlite.COL_ID)); + scheduledBoost.instance = c.getString(c.getColumnIndexOrThrow(Sqlite.COL_INSTANCE)); + scheduledBoost.userId = c.getString(c.getColumnIndexOrThrow(Sqlite.COL_USER_ID)); + scheduledBoost.statusId = c.getString(c.getColumnIndexOrThrow(Sqlite.COL_STATUS_ID)); + scheduledBoost.reblogged = c.getInt(c.getColumnIndexOrThrow(Sqlite.COL_REBLOGGED)); + scheduledBoost.status = restoreStatusFromString(c.getString(c.getColumnIndexOrThrow(Sqlite.COL_STATUS))); + scheduledBoost.scheduledAt = Helper.stringToDate(context, c.getString(c.getColumnIndexOrThrow(Sqlite.COL_SCHEDULED_AT))); + scheduledBoost.workerUuid = ScheduledBoost.restoreUuidFromString(c.getString(c.getColumnIndexOrThrow(Sqlite.COL_WORKER_UUID))); + return scheduledBoost; + } + +} diff --git a/app/src/main/java/app/fedilab/android/client/entities/StatusCache.java b/app/src/main/java/app/fedilab/android/client/entities/StatusCache.java new file mode 100644 index 000000000..0be396d69 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/entities/StatusCache.java @@ -0,0 +1,327 @@ +package app.fedilab.android.client.entities; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; + +import com.google.gson.Gson; +import com.google.gson.annotations.SerializedName; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import app.fedilab.android.client.mastodon.entities.Pagination; +import app.fedilab.android.client.mastodon.entities.Status; +import app.fedilab.android.client.mastodon.entities.Statuses; +import app.fedilab.android.exception.DBException; +import app.fedilab.android.helper.Helper; +import app.fedilab.android.helper.MastodonHelper; +import app.fedilab.android.sqlite.Sqlite; + +public class StatusCache { + + private final SQLiteDatabase db; + @SerializedName("id") + public long id; + @SerializedName("user_id") + public String user_id; + @SerializedName("instance") + public String instance; + @SerializedName("type") + public CacheEnum type; + @SerializedName("status_id") + public String status_id; + @SerializedName("status") + public Status status; + @SerializedName("created_at") + public Date created_at; + @SerializedName("updated_at") + public Date updated_at; + private Context context; + + public StatusCache() { + db = null; + } + + public StatusCache(Context context) { + //Creation of the DB with tables + this.context = context; + this.db = Sqlite.getInstance(context.getApplicationContext(), Sqlite.DB_NAME, null, Sqlite.DB_VERSION).open(); + } + + /** + * Serialized a Status class + * + * @param mastodon_status {@link Status} to serialize + * @return String serialized status + */ + public static String mastodonStatusToStringStorage(Status mastodon_status) { + Gson gson = new Gson(); + try { + return gson.toJson(mastodon_status); + } catch (Exception e) { + return null; + } + } + + /** + * Unserialized a Mastodon Status + * + * @param serializedStatus String serialized status + * @return {@link Status} + */ + public static Status restoreStatusFromString(String serializedStatus) { + Gson gson = new Gson(); + try { + return gson.fromJson(serializedStatus, Status.class); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + /** + * Insert or update a status + * + * @param statusCache {@link StatusCache} + * @return long - db id + * @throws DBException exception with database + */ + public long insertOrUpdate(StatusCache statusCache) throws DBException { + if (db == null) { + throw new DBException("db is null. Wrong initialization."); + } + boolean exists = statusExist(statusCache); + long idReturned; + if (exists) { + idReturned = updateStatus(statusCache); + } else { + idReturned = insertStatus(statusCache); + } + return idReturned; + } + + /** + * Check if a status exists in db + * + * @param statusCache Status {@link StatusCache} + * @return boolean - StatusCache exists + * @throws DBException Exception + */ + public boolean statusExist(StatusCache statusCache) throws DBException { + if (db == null) { + throw new DBException("db is null. Wrong initialization."); + } + Cursor mCount = db.rawQuery("select count(*) from " + Sqlite.TABLE_STATUS_CACHE + + " where " + Sqlite.COL_STATUS_ID + " = '" + statusCache.status_id + "'" + + " AND " + Sqlite.COL_INSTANCE + " = '" + statusCache.instance + "'" + + " AND " + Sqlite.COL_USER_ID + "= '" + statusCache.user_id + "'", null); + mCount.moveToFirst(); + int count = mCount.getInt(0); + mCount.close(); + return (count > 0); + } + + /** + * Insert a status in db + * + * @param statusCache {@link StatusCache} + * @return long - db id + * @throws DBException exception with database + */ + private long insertStatus(StatusCache statusCache) throws DBException { + if (db == null) { + throw new DBException("db is null. Wrong initialization."); + } + ContentValues values = new ContentValues(); + values.put(Sqlite.COL_USER_ID, statusCache.user_id); + values.put(Sqlite.COL_INSTANCE, statusCache.instance); + values.put(Sqlite.COL_TYPE, statusCache.type.getValue()); + values.put(Sqlite.COL_STATUS_ID, statusCache.status_id); + values.put(Sqlite.COL_STATUS, mastodonStatusToStringStorage(statusCache.status)); + values.put(Sqlite.COL_CREATED_AT, Helper.dateToString(new Date())); + //Inserts token + try { + return db.insertOrThrow(Sqlite.TABLE_STATUS_CACHE, null, values); + } catch (Exception e) { + e.printStackTrace(); + return -1; + } + } + + /** + * Update a status in db + * + * @param statusCache {@link StatusCache} + * @return long - db id + * @throws DBException exception with database + */ + private long updateStatus(StatusCache statusCache) throws DBException { + if (db == null) { + throw new DBException("db is null. Wrong initialization."); + } + ContentValues values = new ContentValues(); + values.put(Sqlite.COL_USER_ID, statusCache.user_id); + values.put(Sqlite.COL_TYPE, statusCache.type.getValue()); + values.put(Sqlite.COL_STATUS_ID, statusCache.status_id); + values.put(Sqlite.COL_STATUS, mastodonStatusToStringStorage(statusCache.status)); + values.put(Sqlite.COL_UPDATED_AT, Helper.dateToString(new Date())); + //Inserts token + try { + return db.update(Sqlite.TABLE_STATUS_CACHE, + values, Sqlite.COL_STATUS_ID + " = ? AND " + Sqlite.COL_INSTANCE + " =?", + new String[]{statusCache.status_id, statusCache.instance}); + } catch (Exception e) { + e.printStackTrace(); + return -1; + } + } + + /** + * Get paginated statuses from db + * + * @param type CacheEnum - not used yet but will allow to extend cache to other timelines + * @param instance String - instance + * @param user_id String - us + * @param max_id String - status having max id + * @param min_id String - status having min id + * @return Statuses + * @throws DBException - throws a db exception + */ + public Statuses geStatuses(CacheEnum type, String instance, String user_id, String max_id, String min_id) throws DBException { + if (db == null) { + throw new DBException("db is null. Wrong initialization."); + } + String selection = Sqlite.COL_INSTANCE + "='" + instance + "' AND " + Sqlite.COL_USER_ID + "= '" + user_id + "'"; + String limit = String.valueOf(MastodonHelper.statusesPerCall(context)); + if (max_id == null && min_id != null) { + selection += "AND " + Sqlite.COL_STATUS_ID + " >= '" + min_id + "'"; + } else if (max_id != null && min_id == null) { + selection += "AND " + Sqlite.COL_STATUS_ID + " <= '" + max_id + "'"; + } else if (max_id != null) { + selection += "AND " + Sqlite.COL_STATUS_ID + " >= '" + min_id + "' AND " + Sqlite.COL_STATUS_ID + " <= '" + max_id + "'"; + limit = null; + } + try { + Cursor c = db.query(Sqlite.TABLE_STATUS_CACHE, null, selection, null, null, null, Sqlite.COL_STATUS_ID + " DESC", limit); + return createStatusReply(cursorToListOfStatuses(c)); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + + /** + * @param type CacheEnum - not used yet but will allow to extend cache to other timelines + * @param instance String - instance + * @param user_id String - us + * @param search String search + * @return - List + * @throws DBException exception + */ + public List searchStatus(CacheEnum type, String instance, String user_id, String search) throws DBException { + if (db == null) { + throw new DBException("db is null. Wrong initialization."); + } + String selection = Sqlite.COL_INSTANCE + "='" + instance + + "' AND " + Sqlite.COL_USER_ID + "= '" + user_id + "'"; + List reply = new ArrayList<>(); + try { + Cursor c = db.query(Sqlite.TABLE_STATUS_CACHE, null, selection, null, null, null, Sqlite.COL_STATUS_ID + " DESC", ""); + List statuses = cursorToListOfStatuses(c); + if (statuses != null && statuses.size() > 0) { + for (Status status : statuses) { + if (status.content.toLowerCase().contains(search.trim().toLowerCase())) { + reply.add(status); + } + } + } + } catch (Exception e) { + e.printStackTrace(); + return null; + } + return reply; + } + + /** + * Convert a cursor to list of statuses + * + * @param c Cursor + * @return List + */ + private List cursorToListOfStatuses(Cursor c) { + //No element found + if (c.getCount() == 0) { + c.close(); + return null; + } + List statusList = new ArrayList<>(); + while (c.moveToNext()) { + Status status = convertCursorToStatus(c); + statusList.add(status); + } + //Close the cursor + c.close(); + return statusList; + } + + /** + * Create a reply from db in the same way than API call + * + * @param statusList List + * @return Statuses (with pagination) + */ + private Statuses createStatusReply(List statusList) { + Statuses statuses = new Statuses(); + statuses.statuses = statusList; + Pagination pagination = new Pagination(); + if (statusList != null && statusList.size() > 0) { + pagination.max_id = statusList.get(0).id; + pagination.min_id = statusList.get(statusList.size() - 1).id; + } + statuses.pagination = pagination; + return statuses; + } + + /** + * Read cursor and hydrate without closing it + * + * @param c - Cursor + * @return Status + */ + private Status convertCursorToStatus(Cursor c) { + String serializedStatus = c.getString(c.getColumnIndexOrThrow(Sqlite.COL_STATUS)); + return restoreStatusFromString(serializedStatus); + } + + public enum CacheEnum { + @SerializedName("HOME") + HOME("HOME"); + private final String value; + + CacheEnum(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + } +} diff --git a/app/src/main/java/app/fedilab/android/client/entities/StatusDraft.java b/app/src/main/java/app/fedilab/android/client/entities/StatusDraft.java new file mode 100644 index 000000000..e5d2d0a16 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/entities/StatusDraft.java @@ -0,0 +1,374 @@ +package app.fedilab.android.client.entities; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; + +import com.google.gson.Gson; +import com.google.gson.annotations.SerializedName; +import com.google.gson.reflect.TypeToken; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.UUID; + +import app.fedilab.android.BaseMainActivity; +import app.fedilab.android.client.mastodon.entities.Status; +import app.fedilab.android.exception.DBException; +import app.fedilab.android.helper.Helper; +import app.fedilab.android.sqlite.Sqlite; + + +public class StatusDraft implements Serializable { + + + private transient final SQLiteDatabase db; + @SerializedName("id") + public long id = -1; + @SerializedName("instance") + public String instance; + @SerializedName("user_id") + public String user_id; + @SerializedName("statusDraftList") + public List statusDraftList; + @SerializedName("statusReplyList") + public List statusReplyList; + @SerializedName("state") + public PostState state; + @SerializedName("created_at") + public Date created_ad; + @SerializedName("updated_at") + public Date updated_at; + @SerializedName("worker_uuid") + public UUID workerUuid; + @SerializedName("scheduled_at") + public Date scheduled_at; + + private transient Context context; + + public StatusDraft() { + db = null; + } + + public StatusDraft(Context context) { + //Creation of the DB with tables + this.context = context; + this.db = Sqlite.getInstance(context.getApplicationContext(), Sqlite.DB_NAME, null, Sqlite.DB_VERSION).open(); + } + + /** + * Serialized a list of Status class + * + * @param statuses List of {@link Status} to serialize + * @return String serialized emoji list + */ + public static String mastodonStatusListToStringStorage(List statuses) { + Gson gson = new Gson(); + try { + return gson.toJson(statuses); + } catch (Exception e) { + return null; + } + } + + /** + * Unserialized a Status List + * + * @param serializedStatusList String serialized Status list + * @return List of {@link Status} + */ + public static List restoreStatusListFromString(String serializedStatusList) { + Gson gson = new Gson(); + try { + return gson.fromJson(serializedStatusList, new TypeToken>() { + }.getType()); + } catch (Exception e) { + return null; + } + } + + /** + * Serialized a list of Status class + * + * @param postState {@link PostState} to serialize + * @return String serialized PostState list + */ + public static String postStateToStringStorage(PostState postState) { + Gson gson = new Gson(); + try { + return gson.toJson(postState); + } catch (Exception e) { + return null; + } + } + + /** + * Unserialized a PostState + * + * @param serializedPostState String serialized PostState + * @return {@link PostState} + */ + public static PostState restorePostStateFromString(String serializedPostState) { + Gson gson = new Gson(); + try { + return gson.fromJson(serializedPostState, PostState.class); + } catch (Exception e) { + return null; + } + } + + /** + * Insert statusDraft in db + * + * @param statusDraft {@link StatusDraft} + * @return long - db id + * @throws DBException exception with database + */ + public long insertStatusDraft(StatusDraft statusDraft) throws DBException { + if (db == null) { + throw new DBException("db is null. Wrong initialization."); + } + ContentValues values = new ContentValues(); + values.put(Sqlite.COL_INSTANCE, statusDraft.instance); + values.put(Sqlite.COL_USER_ID, statusDraft.user_id); + values.put(Sqlite.COL_DRAFTS, mastodonStatusListToStringStorage(statusDraft.statusDraftList)); + values.put(Sqlite.COL_REPLIES, mastodonStatusListToStringStorage(statusDraft.statusReplyList)); + values.put(Sqlite.COL_CREATED_AT, Helper.dateToString(new Date())); + if (statusDraft.workerUuid != null) { + values.put(Sqlite.COL_WORKER_UUID, ScheduledBoost.uuidToStringStorage(statusDraft.workerUuid)); + values.put(Sqlite.COL_SCHEDULED_AT, Helper.dateToString(statusDraft.scheduled_at)); + } + //Inserts drafts + try { + return db.insertOrThrow(Sqlite.TABLE_STATUS_DRAFT, null, values); + } catch (Exception e) { + e.printStackTrace(); + return -1; + } + } + + + /** + * Remove a draft from db + * + * @param statusDraft {@link StatusDraft} + * @return int + */ + public int removeDraft(StatusDraft statusDraft) throws DBException { + if (db == null) { + throw new DBException("db is null. Wrong initialization."); + } + return db.delete(Sqlite.TABLE_STATUS_DRAFT, Sqlite.COL_ID + " = '" + statusDraft.id + "'", null); + } + + /** + * Remove all drafts for an account from db + * + * @return int + */ + public int removeAllDraft() throws DBException { + if (db == null) { + throw new DBException("db is null. Wrong initialization."); + } + return db.delete(Sqlite.TABLE_STATUS_DRAFT, Sqlite.COL_USER_ID + " = '" + BaseMainActivity.currentUserID + "' AND " + Sqlite.COL_INSTANCE + " = '" + BaseMainActivity.currentInstance + "'", null); + } + + /** + * update statusDraft in db + * + * @param statusDraft {@link StatusDraft} + * @return long - db id + * @throws DBException exception with database + */ + public long updateStatusDraft(StatusDraft statusDraft) throws DBException { + if (db == null) { + throw new DBException("db is null. Wrong initialization."); + } + ContentValues values = new ContentValues(); + values.put(Sqlite.COL_DRAFTS, mastodonStatusListToStringStorage(statusDraft.statusDraftList)); + values.put(Sqlite.COL_REPLIES, mastodonStatusListToStringStorage(statusDraft.statusReplyList)); + values.put(Sqlite.COL_UPDATED_AT, Helper.dateToString(new Date())); + if (statusDraft.workerUuid != null) { + values.put(Sqlite.COL_WORKER_UUID, ScheduledBoost.uuidToStringStorage(statusDraft.workerUuid)); + values.put(Sqlite.COL_SCHEDULED_AT, Helper.dateToString(statusDraft.scheduled_at)); + } + //Inserts token + try { + return db.update(Sqlite.TABLE_STATUS_DRAFT, + values, Sqlite.COL_ID + " = ?", + new String[]{String.valueOf(statusDraft.id)}); + } catch (Exception e) { + e.printStackTrace(); + return -1; + } + } + + /** + * remove schedule statusDraft in db + * + * @param statusDraft {@link StatusDraft} + * @return long - db id + * @throws DBException exception with database + */ + public long removeScheduled(StatusDraft statusDraft) throws DBException { + if (db == null) { + throw new DBException("db is null. Wrong initialization."); + } + ContentValues values = new ContentValues(); + values.putNull(Sqlite.COL_WORKER_UUID); + values.putNull(Sqlite.COL_SCHEDULED_AT); + //Inserts token + try { + return db.update(Sqlite.TABLE_STATUS_DRAFT, + values, Sqlite.COL_ID + " = ?", + new String[]{String.valueOf(statusDraft.id)}); + } catch (Exception e) { + e.printStackTrace(); + return -1; + } + } + + + /** + * update statusDraft in db + * + * @param statusDraft {@link StatusDraft} + * @return long - db id + * @throws DBException exception with database + */ + public long updatePostState(StatusDraft statusDraft) throws DBException { + if (db == null) { + throw new DBException("db is null. Wrong initialization."); + } + ContentValues values = new ContentValues(); + values.put(Sqlite.COL_STATE, postStateToStringStorage(statusDraft.state)); + values.put(Sqlite.COL_UPDATED_AT, Helper.dateToString(new Date())); + //Inserts token + try { + return db.update(Sqlite.TABLE_STATUS_DRAFT, + values, Sqlite.COL_ID + " = ?", + new String[]{String.valueOf(statusDraft.id)}); + } catch (Exception e) { + e.printStackTrace(); + return -1; + } + } + + + /** + * Returns the StatusDraft for an account + * + * @param account Account + * @return List - List of {@link StatusDraft} + */ + public List geStatusDraftList(Account account) throws DBException { + if (db == null) { + throw new DBException("db is null. Wrong initialization."); + } + try { + Cursor c = db.query(Sqlite.TABLE_STATUS_DRAFT, null, Sqlite.COL_INSTANCE + " = '" + account.instance + "' AND " + Sqlite.COL_USER_ID + " = '" + account.user_id + "'", null, null, null, Sqlite.COL_UPDATED_AT + " ASC", null); + return cursorToStatusDraftList(c); + } catch (Exception e) { + return null; + } + } + + + /** + * Returns the StatusDraft for an account that has been scheduled by the client + * + * @param account Account + * @return List - List of {@link StatusDraft} + */ + public List geStatusDraftScheduledList(Account account) throws DBException { + if (db == null) { + throw new DBException("db is null. Wrong initialization."); + } + try { + Cursor c = db.query(Sqlite.TABLE_STATUS_DRAFT, null, Sqlite.COL_INSTANCE + " = '" + account.instance + "' AND " + Sqlite.COL_USER_ID + " = '" + account.user_id + "' AND " + Sqlite.COL_WORKER_UUID + " != ''", null, null, null, Sqlite.COL_UPDATED_AT + " ASC", null); + return cursorToStatusDraftList(c); + } catch (Exception e) { + return null; + } + } + + /** + * Returns the StatusDraft for an account + * + * @param draftId String + * @return StatusDraft - {@link StatusDraft} + */ + public StatusDraft geStatusDraft(String draftId) throws DBException { + if (db == null) { + throw new DBException("db is null. Wrong initialization."); + } + try { + Cursor c = db.query(Sqlite.TABLE_STATUS_DRAFT, null, Sqlite.COL_ID + " = '" + draftId + "'", null, null, null, null, "1"); + return convertCursorToStatusDraft(c); + } catch (Exception e) { + return null; + } + } + + + /** + * Restore statusDraft list from db + * + * @param c Cursor + * @return List + */ + private List cursorToStatusDraftList(Cursor c) { + //No element found + if (c.getCount() == 0) { + c.close(); + return null; + } + List statusDrafts = new ArrayList<>(); + while (c.moveToNext()) { + StatusDraft statusDraft = convertCursorToStatusDraft(c); + statusDrafts.add(statusDraft); + } + //Close the cursor + c.close(); + return statusDrafts; + } + + + /** + * Read cursor and hydrate without closing it + * + * @param c - Cursor + * @return Timeline + */ + private StatusDraft convertCursorToStatusDraft(Cursor c) { + StatusDraft statusDraft = new StatusDraft(); + statusDraft.id = c.getInt(c.getColumnIndexOrThrow(Sqlite.COL_ID)); + statusDraft.instance = c.getString(c.getColumnIndexOrThrow(Sqlite.COL_INSTANCE)); + statusDraft.user_id = c.getString(c.getColumnIndexOrThrow(Sqlite.COL_USER_ID)); + statusDraft.statusReplyList = restoreStatusListFromString(c.getString(c.getColumnIndexOrThrow(Sqlite.COL_REPLIES))); + statusDraft.statusDraftList = restoreStatusListFromString(c.getString(c.getColumnIndexOrThrow(Sqlite.COL_DRAFTS))); + statusDraft.state = restorePostStateFromString(c.getString(c.getColumnIndexOrThrow(Sqlite.COL_STATE))); + statusDraft.created_ad = Helper.stringToDate(context, c.getString(c.getColumnIndexOrThrow(Sqlite.COL_CREATED_AT))); + statusDraft.updated_at = Helper.stringToDate(context, c.getString(c.getColumnIndexOrThrow(Sqlite.COL_UPDATED_AT))); + statusDraft.workerUuid = ScheduledBoost.restoreUuidFromString(c.getString(c.getColumnIndexOrThrow(Sqlite.COL_WORKER_UUID))); + statusDraft.scheduled_at = Helper.stringToDate(context, c.getString(c.getColumnIndexOrThrow(Sqlite.COL_SCHEDULED_AT))); + return statusDraft; + } +} diff --git a/app/src/main/java/app/fedilab/android/client/entities/Timeline.java b/app/src/main/java/app/fedilab/android/client/entities/Timeline.java new file mode 100644 index 000000000..bd610973a --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/entities/Timeline.java @@ -0,0 +1,419 @@ +package app.fedilab.android.client.entities; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; + +import com.google.gson.Gson; +import com.google.gson.annotations.SerializedName; + +import java.util.ArrayList; +import java.util.List; + +import app.fedilab.android.exception.DBException; +import app.fedilab.android.sqlite.Sqlite; + +public class Timeline { + + private final SQLiteDatabase db; + @SerializedName("id") + public long id; + @SerializedName("user_id") + public String user_id; + @SerializedName("instance") + public String instance; + @SerializedName("position") + public int position; + @SerializedName("type") + public TimeLineEnum type; + @SerializedName("remote_instance") + public String remote_instance; + @SerializedName("displayed") + public boolean displayed; + @SerializedName("timelineOptions") + public TimelineOptions timelineOptions; + private Context context; + + public Timeline() { + db = null; + } + + public Timeline(Context context) { + //Creation of the DB with tables + this.context = context; + this.db = Sqlite.getInstance(context.getApplicationContext(), Sqlite.DB_NAME, null, Sqlite.DB_VERSION).open(); + } + + /** + * Serialized a TimelineOptions class + * + * @param timelineOptions {@link TimelineOptions} to serialize + * @return String serialized timeline options + */ + public static String timelineOptionsToStringStorage(TimelineOptions timelineOptions) { + Gson gson = new Gson(); + try { + return gson.toJson(timelineOptions); + } catch (Exception e) { + return null; + } + } + + /** + * Unserialized a TimelineOptions + * + * @param serializedTimelineOptionsString serialized timeline options + * @return {@link TimelineOptions} + */ + public static TimelineOptions restoreTimelineOptionsFromString(String serializedTimelineOptionsString) { + Gson gson = new Gson(); + try { + return gson.fromJson(serializedTimelineOptionsString, TimelineOptions.class); + } catch (Exception e) { + return null; + } + } + + /** + * Insert a timeline + * + * @param timeline {@link Timeline} + * @return long - db id + * @throws DBException exception with database + */ + public long insert(Timeline timeline) throws DBException { + if (db == null) { + throw new DBException("db is null. Wrong initialization."); + } + if (!canBeModified(timeline)) { + return -1; + } + ContentValues values = new ContentValues(); + values.put(Sqlite.COL_POSITION, countEntries()); + values.put(Sqlite.COL_USER_ID, timeline.user_id); + values.put(Sqlite.COL_INSTANCE, timeline.instance); + values.put(Sqlite.COL_TYPE, timeline.type.getValue()); + values.put(Sqlite.COL_REMOTE_INSTANCE, timeline.remote_instance); + values.put(Sqlite.COL_DISPLAYED, timeline.displayed); + if (timeline.timelineOptions != null) { + values.put(Sqlite.COL_TIMELINE_OPTION, timelineOptionsToStringStorage(timeline.timelineOptions)); + } + try { + return db.insertOrThrow(Sqlite.TABLE_TIMELINES, null, values); + } catch (Exception e) { + e.printStackTrace(); + return -1; + } + } + + private boolean canBeModified(Timeline timeline) { + return timeline.type != TimeLineEnum.HOME && timeline.type != TimeLineEnum.DIRECT && timeline.type != TimeLineEnum.LOCAL && timeline.type != TimeLineEnum.PUBLIC && timeline.type != TimeLineEnum.NOTIFICATION; + } + + /** + * update a timeline + * + * @param timeline {@link Timeline} + * @return long - db id + * @throws DBException exception with database + */ + public long update(Timeline timeline) throws DBException { + if (db == null) { + throw new DBException("db is null. Wrong initialization."); + } + ContentValues values = new ContentValues(); + values.put(Sqlite.COL_POSITION, timeline.position); + values.put(Sqlite.COL_DISPLAYED, timeline.displayed); + if (timeline.timelineOptions != null && canBeModified(timeline)) { + values.put(Sqlite.COL_TIMELINE_OPTION, timelineOptionsToStringStorage(timeline.timelineOptions)); + } + reorderUpdatePosition(timeline); + try { + return db.update(Sqlite.TABLE_TIMELINES, + values, Sqlite.COL_ID + " = ?", + new String[]{String.valueOf(timeline.id)}); + } catch (Exception e) { + e.printStackTrace(); + return -1; + } + } + + + /** + * Remove a timeline from db + * + * @param timeline {@link Timeline} + * @return int + */ + public int remove(Timeline timeline) throws DBException { + if (db == null) { + throw new DBException("db is null. Wrong initialization."); + } + if (!canBeModified(timeline)) { + return -1; + } + reorderDeletePosition(timeline); + return db.delete(Sqlite.TABLE_TIMELINES, Sqlite.COL_ID + " = '" + timeline.id + "'", null); + } + + /** + * Returns all timelines between two position (positions included) + * + * @return List timelines + */ + public List getTimelineBetweenPosition(int min, int max) throws DBException { + if (db == null) { + throw new DBException("db is null. Wrong initialization."); + } + if (min > max) { + int _t = min; + min = max; + max = min; + } + try { + Cursor c = db.query(Sqlite.TABLE_TIMELINES, null, Sqlite.COL_POSITION + " >= '" + min + "' AND " + Sqlite.COL_POSITION + " <= '" + max + "'", null, null, null, Sqlite.COL_POSITION + " ASC", null); + return cursorToListTimelines(c); + } catch (Exception e) { + return null; + } + } + + + /** + * Returns all timelines after a position (position included) + * + * @return List timelines + */ + public List getTimelineAfterPosition(int position) throws DBException { + if (db == null) { + throw new DBException("db is null. Wrong initialization."); + } + try { + Cursor c = db.query(Sqlite.TABLE_TIMELINES, null, Sqlite.COL_POSITION + " > '" + position + "'", null, null, null, Sqlite.COL_POSITION + " ASC", null); + return cursorToListTimelines(c); + } catch (Exception e) { + return null; + } + } + + /** + * Reorder each position after moving an element + * + * @param _mTimeline Timeline + * @throws DBException - db exception + */ + public void reorderUpdatePosition(Timeline _mTimeline) throws DBException { + Timeline previousPosition = getTimeline(_mTimeline.id); + List timelines = getTimelineBetweenPosition(_mTimeline.position, previousPosition.position); + if (previousPosition.position > _mTimeline.position) { + for (int i = _mTimeline.position; i < timelines.size(); i++) { + Timeline timeline = timelines.get(i); + timeline.position++; + update(timeline); + } + } else if (previousPosition.position < _mTimeline.position) { + for (int i = previousPosition.position + 1; i <= timelines.size(); i++) { + Timeline timeline = timelines.get(i); + timeline.position--; + update(timeline); + } + } + } + + /** + * Reorder each position after deleting an element + * + * @param _mTimeline Timeline + * @throws DBException - db exception + */ + public void reorderDeletePosition(Timeline _mTimeline) throws DBException { + List timelines = getTimelineAfterPosition(_mTimeline.position); + for (Timeline timeline : timelines) { + timeline.position--; + update(timeline); + } + } + + /** + * Returns all timelines + * + * @return List timelines + */ + public List getTimelines() throws DBException { + if (db == null) { + throw new DBException("db is null. Wrong initialization."); + } + try { + Cursor c = db.query(Sqlite.TABLE_TIMELINES, null, null, null, null, null, Sqlite.COL_POSITION + " ASC", null); + return cursorToListTimelines(c); + } catch (Exception e) { + return null; + } + } + + /** + * Returns a timeline + * + * @return Timelines timeline + */ + public Timeline getTimeline(long id) throws DBException { + if (db == null) { + throw new DBException("db is null. Wrong initialization."); + } + try { + Cursor c = db.query(Sqlite.TABLE_TIMELINES, null, Sqlite.COL_ID + "='" + id + "'", null, null, null, Sqlite.COL_POSITION + " ASC", null); + return cursorToTimeline(c); + } catch (Exception e) { + return null; + } + } + + private List cursorToListTimelines(Cursor c) { + //No element found + if (c.getCount() == 0) { + c.close(); + return null; + } + List timelineList = new ArrayList<>(); + while (c.moveToNext()) { + Timeline timeline = convertCursorToTimeLine(c); + timelineList.add(timeline); + } + //Close the cursor + c.close(); + return timelineList; + } + + private Timeline cursorToTimeline(Cursor c) { + //No element found + if (c.getCount() == 0) { + c.close(); + return null; + } + //Take the first element + c.moveToFirst(); + Timeline timeline = convertCursorToTimeLine(c); + //Close the cursor + c.close(); + return timeline; + } + + /** + * Read cursor and hydrate without closing it + * + * @param c - Cursor + * @return Timeline + */ + private Timeline convertCursorToTimeLine(Cursor c) { + Timeline timeline = new Timeline(); + timeline.id = c.getInt(c.getColumnIndexOrThrow(Sqlite.COL_ID)); + timeline.timelineOptions = restoreTimelineOptionsFromString(c.getString(c.getColumnIndexOrThrow(Sqlite.COL_TIMELINE_OPTION))); + timeline.type = TimeLineEnum.valueOf(c.getString(c.getColumnIndexOrThrow(Sqlite.COL_TYPE))); + timeline.displayed = c.getInt(c.getColumnIndexOrThrow(Sqlite.COL_TIMELINE_OPTION)) == 1; + timeline.instance = c.getString(c.getColumnIndexOrThrow(Sqlite.COL_INSTANCE)); + timeline.user_id = c.getString(c.getColumnIndexOrThrow(Sqlite.COL_USER_ID)); + timeline.remote_instance = c.getString(c.getColumnIndexOrThrow(Sqlite.COL_REMOTE_INSTANCE)); + timeline.position = c.getInt(c.getColumnIndexOrThrow(Sqlite.COL_POSITION)); + return timeline; + } + + + /** + * Count entry in db + * + * @return int - number of timelines recorded in db + * @throws DBException Exception + */ + public int countEntries() throws DBException { + if (db == null) { + throw new DBException("db is null. Wrong initialization."); + } + Cursor mCount = db.rawQuery("select count(*) from " + Sqlite.TABLE_TIMELINES, null); + mCount.moveToFirst(); + int count = mCount.getInt(0); + mCount.close(); + return count; + } + + + public enum TimeLineEnum { + @SerializedName("HOME") + HOME("HOME"), + @SerializedName("DIRECT") + DIRECT("DIRECT"), + @SerializedName("NOTIFICATION") + NOTIFICATION("NOTIFICATION"), + @SerializedName("LOCAL") + LOCAL("LOCAL"), + @SerializedName("PUBLIC") + PUBLIC("PUBLIC"), + @SerializedName("TAG") + TAG("TAG"), + @SerializedName("LIST") + LIST("LIST"), + @SerializedName("REMOTE") + REMOTE("REMOTE"), + @SerializedName("ACCOUNT_TIMELINE") + ACCOUNT_TIMELINE("ACCOUNT_TIMELINE"), + @SerializedName("MUTED_TIMELINE") + MUTED_TIMELINE("MUTED_TIMELINE"), + @SerializedName("BOOKMARK_TIMELINE") + BOOKMARK_TIMELINE("BOOKMARK_TIMELINE"), + @SerializedName("BLOCKED_TIMELINE") + BLOCKED_TIMELINE("BLOCKED_TIMELINE"), + @SerializedName("FAVOURITE_TIMELINE") + FAVOURITE_TIMELINE("FAVOURITE_TIMELINE"), + @SerializedName("REBLOG_TIMELINE") + REBLOG_TIMELINE("REBLOG_TIMELINE"), + @SerializedName("SCHEDULED_TOOT_SERVER") + SCHEDULED_TOOT_SERVER("SCHEDULED_TOOT_SERVER"), + @SerializedName("SCHEDULED_TOOT_CLIENT") + SCHEDULED_TOOT_CLIENT("SCHEDULED_TOOT_CLIENT"), + @SerializedName("SCHEDULED_BOOST") + SCHEDULED_BOOST("SCHEDULED_BOOST"); + + + private final String value; + + TimeLineEnum(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + } + + public static class TimelineOptions { + @SerializedName("all") + public List all; + @SerializedName("any") + public List any; + @SerializedName("none") + public List none; + @SerializedName("data") + public List data; + @SerializedName("media_only") + public boolean media_only; + @SerializedName("sensitive") + public boolean sensitive; + @SerializedName("list_id") + public String list_id; + } + +} diff --git a/app/src/main/java/app/fedilab/android/client/entities/WellKnownNodeinfo.java b/app/src/main/java/app/fedilab/android/client/entities/WellKnownNodeinfo.java new file mode 100644 index 000000000..1935414c9 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/entities/WellKnownNodeinfo.java @@ -0,0 +1,78 @@ +package app.fedilab.android.client.entities; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import com.google.gson.annotations.SerializedName; + +import java.util.List; + +public class WellKnownNodeinfo { + + @SerializedName("links") + public List links; + + public static class NodeInfoLinks { + @SerializedName("reel") + public String reel; + @SerializedName("href") + public String href; + } + + public static class NodeInfo { + @SerializedName("version") + public String version; + @SerializedName("software") + public Software software; + @SerializedName("usage") + public Usage usage; + @SerializedName("metadata") + public Metadata metadata; + @SerializedName("openRegistrations") + public boolean openRegistrations; + + } + + public static class Software { + @SerializedName("name") + public String name; + @SerializedName("version") + public String version; + } + + public static class Usage { + @SerializedName("users") + public Users users; + @SerializedName("localPosts") + public int localPosts; + } + + public static class Users { + @SerializedName("total") + public int total; + @SerializedName("activeMonth") + public int activeMonth; + @SerializedName("activeHalfyear") + public int activeHalfyear; + } + + public static class Metadata { + @SerializedName("nodeName") + public String nodeName; + @SerializedName("nodeDescription") + public String nodeDescription; + @SerializedName("staffAccounts") + public List staffAccounts; + } +} diff --git a/app/src/main/java/app/fedilab/android/client/entities/app/PinnedTimeline.java b/app/src/main/java/app/fedilab/android/client/entities/app/PinnedTimeline.java new file mode 100644 index 000000000..210f91526 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/entities/app/PinnedTimeline.java @@ -0,0 +1,49 @@ +package app.fedilab.android.client.entities.app; +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import com.google.gson.annotations.SerializedName; + +import java.io.Serializable; + +import app.fedilab.android.client.entities.Timeline; +import app.fedilab.android.client.mastodon.entities.MastodonList; + +public class PinnedTimeline implements Serializable { + + @SerializedName("id") + public int id; + @SerializedName("userId") + public String userId; + @SerializedName("instance") + public String instance; + @SerializedName("position") + public int position; + @SerializedName("displayed") + public boolean displayed = true; + @SerializedName("type") + public Timeline.TimeLineEnum type; + @SerializedName("remoteInstance") + public RemoteInstance remoteInstance; + @SerializedName("tagTimeline") + public TagTimeline tagTimeline; + @SerializedName("mastodonList") + public MastodonList mastodonList; + @SerializedName("currentFilter") + public String currentFilter; + + + public transient boolean isSelected = false; +} diff --git a/app/src/main/java/app/fedilab/android/client/entities/app/RemoteInstance.java b/app/src/main/java/app/fedilab/android/client/entities/app/RemoteInstance.java new file mode 100644 index 000000000..39b73df47 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/entities/app/RemoteInstance.java @@ -0,0 +1,62 @@ +package app.fedilab.android.client.entities.app; +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import com.google.gson.annotations.SerializedName; + +import java.io.Serializable; +import java.util.List; + +public class RemoteInstance implements Serializable { + + @SerializedName("dbID") + public long dbID; + @SerializedName("id") + public String id; + @SerializedName("host") + public String host; + @SerializedName("type") + public InstanceType type; + @SerializedName("tags") + public List tags; + @SerializedName("filteredWith") + public String filteredWith; + + + public enum InstanceType { + @SerializedName("MASTODON") + MASTODON("MASTODON"), + @SerializedName("PIXELFED") + PIXELFED("PIXELFED"), + @SerializedName("PEERTUBE") + PEERTUBE("PEERTUBE"), + @SerializedName("NITTER") + NITTER("NITTER"), + @SerializedName("MISSKEY") + MISSKEY("MISSKEY"), + @SerializedName("GNU") + GNU("GNU"); + + private final String value; + + InstanceType(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + } +} diff --git a/app/src/main/java/app/fedilab/android/client/entities/app/TagTimeline.java b/app/src/main/java/app/fedilab/android/client/entities/app/TagTimeline.java new file mode 100644 index 000000000..9a96ac617 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/entities/app/TagTimeline.java @@ -0,0 +1,40 @@ +package app.fedilab.android.client.entities.app; +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import com.google.gson.annotations.SerializedName; + +import java.io.Serializable; +import java.util.List; + +public class TagTimeline implements Serializable { + @SerializedName("id") + public int id; + @SerializedName("name") + public String name; + @SerializedName("displayName") + public String displayName; + @SerializedName("isART") + public boolean isART; + @SerializedName("isNSFW") + public boolean isNSFW; + @SerializedName("any") + public List any; + @SerializedName("all") + public List all; + @SerializedName("none") + public List none; + +} diff --git a/app/src/main/java/app/fedilab/android/client/mastodon/JoinMastodonService.java b/app/src/main/java/app/fedilab/android/client/mastodon/JoinMastodonService.java new file mode 100644 index 000000000..bbde5017d --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/mastodon/JoinMastodonService.java @@ -0,0 +1,34 @@ +package app.fedilab.android.client.mastodon; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + + +import java.util.List; + +import app.fedilab.android.client.mastodon.entities.JoinMastodonInstance; +import retrofit2.Call; +import retrofit2.http.GET; +import retrofit2.http.Query; + +public interface JoinMastodonService { + + + @GET("servers") + Call> getInstances( + @Query("category") String category + ); + + +} diff --git a/app/src/main/java/app/fedilab/android/client/mastodon/MastodonAccountsService.java b/app/src/main/java/app/fedilab/android/client/mastodon/MastodonAccountsService.java new file mode 100644 index 000000000..cb5bbcbc4 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/mastodon/MastodonAccountsService.java @@ -0,0 +1,447 @@ +package app.fedilab.android.client.mastodon; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + + +import java.util.List; + +import app.fedilab.android.client.mastodon.entities.Account; +import app.fedilab.android.client.mastodon.entities.FeaturedTag; +import app.fedilab.android.client.mastodon.entities.Filter; +import app.fedilab.android.client.mastodon.entities.IdentityProof; +import app.fedilab.android.client.mastodon.entities.MastodonList; +import app.fedilab.android.client.mastodon.entities.Preferences; +import app.fedilab.android.client.mastodon.entities.RelationShip; +import app.fedilab.android.client.mastodon.entities.Report; +import app.fedilab.android.client.mastodon.entities.Status; +import app.fedilab.android.client.mastodon.entities.Tag; +import app.fedilab.android.client.mastodon.entities.Token; +import okhttp3.MultipartBody; +import retrofit2.Call; +import retrofit2.http.Body; +import retrofit2.http.DELETE; +import retrofit2.http.Field; +import retrofit2.http.FormUrlEncoded; +import retrofit2.http.GET; +import retrofit2.http.Header; +import retrofit2.http.Headers; +import retrofit2.http.Multipart; +import retrofit2.http.PATCH; +import retrofit2.http.POST; +import retrofit2.http.PUT; +import retrofit2.http.Part; +import retrofit2.http.Path; +import retrofit2.http.Query; + +public interface MastodonAccountsService { + + + /* + * Accounts + */ + //Register account + @FormUrlEncoded + @POST("accounts") + Call registerAccount( + @Header("Authorization") String app_token, + @Field("username") String username, + @Field("email") String email, + @Field("password") String password, + @Field("agreement") boolean agreement, + @Field("locale") String locale, + @Field("reason") String reason); + + //Info about the connected account + @GET("accounts/verify_credentials") + Call verify_credentials( + @Header("Authorization") String token); + + @Multipart + @PATCH("accounts/update_credentials") + Call update_media( + @Header("Authorization") String token, + @Part MultipartBody.Part avatar, + @Part MultipartBody.Part header + + ); + + + @Headers({"Accept: application/json"}) + @PATCH("accounts/update_credentials") + Call update_credentials( + @Header("Authorization") String token, @Body Account.AccountParams accountParams + ); + + @FormUrlEncoded + @PATCH("accounts/update_credentials") + Call update_credentials( + @Header("Authorization") String token, + @Field("discoverable") Boolean discoverable, + @Field("bot") Boolean bot, + @Field("display_name") String display_name, + @Field("note") String note, + @Field("locked") Boolean locked, + @Field("source[privacy]") String privacy, + @Field("source[sensitive]") Boolean sensitive, + @Field("source[language]") String language, + @Field("fields_attributes") List fields + ); + + //Get Account + @GET("accounts/{id}") + Call getAccount( + @Header("Authorization") String token, + @Path("id") String id + ); + + //Get Account statuses + @GET("accounts/{id}/statuses") + Call> getAccountStatuses( + @Header("Authorization") String token, + @Path("id") String id, + @Query("max_id") String max_id, + @Query("since_id") String since_id, + @Query("min_id") String min_id, + @Query("exclude_replies") Boolean exclude_replies, + @Query("exclude_reblogs") Boolean exclude_reblogs, + @Query("only_media") Boolean only_media, + @Query("pinned") Boolean pinned, + @Query("limit") int limit + ); + + //Get Account followers + @GET("accounts/{id}/followers") + Call> getAccountFollowers( + @Header("Authorization") String token, + @Path("id") String id, + @Query("max_id") String max_id, + @Query("since_id") String since_id + ); + + //Get Account following + @GET("accounts/{id}/following") + Call> getAccountFollowing( + @Header("Authorization") String token, + @Path("id") String id, + @Query("max_id") String max_id, + @Query("since_id") String since_id + ); + + //Get Account featured tags + @GET("accounts/{id}/featured_tags") + Call> getAccountFeaturedTags( + @Header("Authorization") String token, + @Path("id") String id + ); + + //Lists containing this account + @GET("accounts/{id}/lists") + Call> getListContainingAccount( + @Header("Authorization") String token, + @Path("id") String id + ); + + //Get Identity proofs + @GET("accounts/{id}/identity_proofs") + Call> getIdentityProofs( + @Header("Authorization") String token, + @Path("id") String id + ); + + //Follow account + @FormUrlEncoded + @POST("accounts/{id}/follow") + Call follow( + @Header("Authorization") String app_token, + @Path("id") String id, + @Field("reblogs") boolean reblogs, + @Field("notify") boolean notify + ); + + //Follow account + @FormUrlEncoded + @POST("accounts/{id}/note") + Call note( + @Header("Authorization") String app_token, + @Path("id") String id, + @Field("comment") boolean comment + ); + + //Unfollow account + @POST("accounts/{id}/unfollow") + Call unfollow( + @Header("Authorization") String app_token, + @Path("id") String id + ); + + //Block account + @POST("accounts/{id}/block") + Call block( + @Header("Authorization") String app_token, + @Path("id") String id + ); + + //Unblock account + @POST("accounts/{id}/unblock") + Call unblock( + @Header("Authorization") String app_token, + @Path("id") String id + ); + + //Mute account + @FormUrlEncoded + @POST("accounts/{id}/mute") + Call mute( + @Header("Authorization") String app_token, + @Path("id") String id, + @Field("notifications") Boolean notifications, + @Field("duration") Integer duration + ); + + //Unmute account + @POST("accounts/{id}/unmute") + Call unmute( + @Header("Authorization") String app_token, + @Path("id") String id + ); + + //Feature on profile + @POST("accounts/{id}/pin") + Call endorse( + @Header("Authorization") String app_token, + @Path("id") String id + ); + + //Unfeature account + @POST("accounts/{id}/unpin") + Call unendorse( + @Header("Authorization") String app_token, + @Path("id") String id + ); + + //User note + @FormUrlEncoded + @POST("accounts/{id}/note") + Call note( + @Header("Authorization") String app_token, + @Path("id") String id, + @Field("comment") String comment + ); + + //Get relationships + @GET("accounts/relationships") + Call> getRelationships( + @Header("Authorization") String token, + @Query("id[]") List ids + ); + + //Get search + @GET("accounts/search") + Call> searchAccounts( + @Header("Authorization") String token, + @Query("q") String q, + @Query("limit") int limit, + @Query("resolve") boolean resolve, + @Query("following") boolean following + ); + + + //Bookmarks + @GET("bookmarks") + Call> getBookmarks( + @Header("Authorization") String token, + @Query("limit") String limit, + @Query("max_id") String max_id, + @Query("since_id") String since_id, + @Query("min_id") String min_id + ); + + //favourites + @GET("favourites") + Call> getFavourites( + @Header("Authorization") String token, + @Query("limit") String limit, + @Query("min_id") String min_id, + @Query("max_id") String max_id + ); + + //muted users + @GET("mutes") + Call> getMutes( + @Header("Authorization") String token, + @Query("limit") String limit, + @Query("max_id") String max_id, + @Query("since_id") String since_id + ); + + //Blocked users + @GET("blocks") + Call> getBlocks( + @Header("Authorization") String token, + @Query("limit") String limit, + @Query("max_id") String max_id, + @Query("since_id") String since_id + ); + + //Get blocked domains + @GET("domain_blocks") + Call> getDomainBlocks( + @Header("Authorization") String token, + @Query("limit") String limit, + @Query("max_id") String max_id, + @Query("since_id") String since_id + ); + + //Add a blocked domains + @FormUrlEncoded + @POST("domain_blocks") + Call addDomainBlock( + @Header("Authorization") String token, + @Field("domain") String domain + ); + + //Remove a blocked domains + @DELETE("domain_blocks") + Call removeDomainBlocks( + @Header("Authorization") String token, + @Field("domain") String domain + ); + + //Get filters + @GET("filters") + Call> getFilters( + @Header("Authorization") String token); + + //Get a filter with its id + @GET("filters/{id}") + Call getFilter( + @Header("Authorization") String token, + @Path("id") String id); + + //Add a filter + @FormUrlEncoded + @POST("filters") + Call addFilter( + @Header("Authorization") String token, + @Field("phrase") String phrase, + @Field("context[]") List context, + @Field("irreversible") boolean irreversible, + @Field("whole_word") boolean whole_word, + @Field("expires_in") long expires_in + ); + + //Edit a filter + @FormUrlEncoded + @PUT("filters/{id}") + Call editFilter( + @Header("Authorization") String token, + @Path("id") String id, + @Field("phrase") String phrase, + @Field("context[]") List context, + @Field("irreversible") boolean irreversible, + @Field("whole_word") boolean whole_word, + @Field("expires_in") long expires_in + ); + + //Remove a filter + @DELETE("filters/{id}") + Call removeFilter( + @Header("Authorization") String token, + @Path("id") String id + ); + + //Post a report + @Headers({"Accept: application/json"}) + @POST("reports") + Call report( + @Header("Authorization") String token, @Body Report.ReportParams params + ); + + //Get follow request + @GET("follow_requests") + Call> getFollowRequests( + @Header("Authorization") String token, + @Path("limit") String limit); + + //Accept follow request + @POST("follow_requests/{id}/authorize") + Call acceptFollow( + @Header("Authorization") String token, + @Path("id") String id + ); + + //Reject follow request + @POST("follow_requests/{id}/reject") + Call rejectFollow( + @Header("Authorization") String token, + @Path("id") String id + ); + + //Accounts that the user is currently featuring on their profile. + @GET("endorsements") + Call> getEndorsements( + @Header("Authorization") String token, + @Query("limit") String limit, + @Query("max_id") String max_id, + @Query("since_id") String since_id + ); + + //Feature tags + @GET("featured_tags") + Call> getFeaturedTags( + @Header("Authorization") String token + ); + + //Add a feature tags + @FormUrlEncoded + @POST("featured_tags") + Call addFeaturedTag( + @Header("Authorization") String token, + @Field("name") String name + ); + + //Remove a feature tags + @DELETE("featured_tags/{id}") + Call removeFeaturedTag( + @Header("Authorization") String token, + @Path("id") String id + ); + + //Feature tags suggestions + @GET("featured_tags/suggestions") + Call> getFeaturedTagsSuggestions( + @Header("Authorization") String token + ); + + //Get user preferences + @GET("preferences") + Call getPreferences( + @Header("Authorization") String token + ); + + //Get user suggestions + @GET("suggestions") + Call> getSuggestions( + @Header("Authorization") String token, + @Query("limit") String limit + ); + + //Remove a user suggestion + @DELETE("suggestions/{account_id}") + Call removeSuggestion( + @Header("Authorization") String token, + @Path("account_id") String account_id + ); +} diff --git a/app/src/main/java/app/fedilab/android/client/mastodon/MastodonAdminService.java b/app/src/main/java/app/fedilab/android/client/mastodon/MastodonAdminService.java new file mode 100644 index 000000000..c82621ef3 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/mastodon/MastodonAdminService.java @@ -0,0 +1,149 @@ +package app.fedilab.android.client.mastodon; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + + +import java.util.List; + +import app.fedilab.android.client.mastodon.entities.AdminAccount; +import app.fedilab.android.client.mastodon.entities.AdminReport; +import retrofit2.Call; +import retrofit2.http.Field; +import retrofit2.http.FormUrlEncoded; +import retrofit2.http.GET; +import retrofit2.http.Header; +import retrofit2.http.POST; +import retrofit2.http.Path; +import retrofit2.http.Query; + +public interface MastodonAdminService { + + @GET("/admin/accounts") + Call> getAccounts( + @Header("Authorization") String token, + @Query("local") boolean local, + @Query("remote") boolean remote, + @Query("by_domain") String by_domain, + @Query("active") boolean active, + @Query("pending") boolean pending, + @Query("disabled") boolean disabled, + @Query("silenced") boolean silenced, + @Query("suspended") boolean suspended, + @Query("username") String username, + @Query("display_name") String display_name, + @Query("email") String email, + @Query("ip") String ip, + @Query("staff") boolean staff, + @Query("max_id") String max_id, + @Query("since_id") String since_id, + @Query("limit") int limit + ); + + @GET("/admin/accounts/{id}") + Call getAccount( + @Header("Authorization") String token, + @Path("id") String id + ); + + @POST("/admin/accounts/{account_id}/action") + Call performAction( + @Header("Authorization") String app_token, + @Path("account_id") String account_id, + @Field("type") String type, + @Field("report_id") String report_id, + @Field("warning_preset_id") String warning_preset_id, + @Field("text") String text, + @Field("send_email_notification") boolean send_email_notification + ); + + @FormUrlEncoded + @POST("/admin/accounts/{account_id}/approve") + Call approve( + @Header("Authorization") String app_token, + @Path("account_id") String account_id + ); + + @FormUrlEncoded + @POST("/admin/accounts/{account_id}/reject") + Call reject( + @Header("Authorization") String app_token, + @Path("account_id") String account_id + ); + + @FormUrlEncoded + @POST("/admin/accounts/{account_id}/enable") + Call enable( + @Header("Authorization") String app_token, + @Path("account_id") String account_id + ); + + @FormUrlEncoded + @POST("/admin/accounts/{account_id}/unsilence") + Call unsilence( + @Header("Authorization") String app_token, + @Path("account_id") String account_id + ); + + @FormUrlEncoded + @POST("/admin/accounts/{account_id}/unsuspend") + Call unsuspend( + @Header("Authorization") String app_token, + @Path("account_id") String account_id + ); + + @FormUrlEncoded + @GET("/admin/reports") + Call> getReports( + @Header("Authorization") String token, + @Field("resolved") boolean resolved, + @Field("account_id") String account_id, + @Field("target_account_id") String target_account_id + ); + + @FormUrlEncoded + @GET("/admin/reports/{id}") + Call getReport( + @Header("Authorization") String token, + @Path("id") String id + ); + + @FormUrlEncoded + @POST("/admin/reports/{id}/assign_to_self") + Call assignToSelf( + @Header("Authorization") String app_token, + @Path("id") String id + ); + + @FormUrlEncoded + @POST("/admin/reports/{id}/unassign") + Call unassign( + @Header("Authorization") String app_token, + @Path("id") String id + ); + + @FormUrlEncoded + @POST("/admin/reports/{id}/resolve") + Call resolved( + @Header("Authorization") String app_token, + @Path("id") String id + ); + + @FormUrlEncoded + @POST("/admin/reports/{id}/reopen") + Call reopen( + @Header("Authorization") String app_token, + @Path("id") String id + ); +} diff --git a/app/src/main/java/app/fedilab/android/client/mastodon/MastodonAnnouncementsService.java b/app/src/main/java/app/fedilab/android/client/mastodon/MastodonAnnouncementsService.java new file mode 100644 index 000000000..bbe596e7a --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/mastodon/MastodonAnnouncementsService.java @@ -0,0 +1,61 @@ +package app.fedilab.android.client.mastodon; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + + +import java.util.List; + +import app.fedilab.android.client.mastodon.entities.Announcement; +import retrofit2.Call; +import retrofit2.http.DELETE; +import retrofit2.http.FormUrlEncoded; +import retrofit2.http.GET; +import retrofit2.http.Header; +import retrofit2.http.POST; +import retrofit2.http.PUT; +import retrofit2.http.Path; +import retrofit2.http.Query; + +public interface MastodonAnnouncementsService { + + @GET("/announcements") + Call> getAnnouncements( + @Header("Authorization") String token, + @Query("with_dismissed") boolean with_dismissed + ); + + @FormUrlEncoded + @POST("/announcements/{id}/dismiss") + Call dismiss( + @Header("Authorization") String app_token, + @Path("id") String id + ); + + @FormUrlEncoded + @PUT("/announcements/{id}/reactions/{name}") + Call addReaction( + @Header("Authorization") String app_token, + @Path("id") String id, + @Path("name") String name + ); + + @DELETE("/announcements/{id}/reactions/{name}") + Call removeReaction( + @Header("Authorization") String app_token, + @Path("id") String id, + @Path("name") String name + ); + +} diff --git a/app/src/main/java/app/fedilab/android/client/mastodon/MastodonAppsService.java b/app/src/main/java/app/fedilab/android/client/mastodon/MastodonAppsService.java new file mode 100644 index 000000000..784b7f451 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/mastodon/MastodonAppsService.java @@ -0,0 +1,66 @@ +package app.fedilab.android.client.mastodon; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + + +import app.fedilab.android.client.mastodon.entities.App; +import app.fedilab.android.client.mastodon.entities.Token; +import retrofit2.Call; +import retrofit2.http.Field; +import retrofit2.http.FormUrlEncoded; +import retrofit2.http.GET; +import retrofit2.http.Header; +import retrofit2.http.POST; + +public interface MastodonAppsService { + + + /* + * OAUTH - TOKEN + */ + //Create app + @FormUrlEncoded + @POST("apps") + Call createApp( + @Field("client_name") String client_name, + @Field("redirect_uris") String redirect_uris, + @Field("scopes") String scopes, + @Field("website") String website); + + @GET("apps/verify_credentials") + Call verifyCredentials( + @Header("Authorization") String app_token); + + + //Create token + @FormUrlEncoded + @POST("oauth/token") + Call createToken( + @Field("grant_type") String grant_type, + @Field("client_id") String client_id, + @Field("client_secret") String client_secret, + @Field("redirect_uri") String redirect_uri, + @Field("scope") String scope, + @Field("code") String code); + + //Revoke token + @FormUrlEncoded + @POST("oauth/revoke") + Call revokeToken( + @Field("client_id") String client_id, + @Field("client_secret") String client_secret, + @Field("token") String token); + +} diff --git a/app/src/main/java/app/fedilab/android/client/mastodon/MastodonInstanceService.java b/app/src/main/java/app/fedilab/android/client/mastodon/MastodonInstanceService.java new file mode 100644 index 000000000..4c8014e35 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/mastodon/MastodonInstanceService.java @@ -0,0 +1,54 @@ +package app.fedilab.android.client.mastodon; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + + +import java.util.List; + +import app.fedilab.android.client.mastodon.entities.Account; +import app.fedilab.android.client.mastodon.entities.Activity; +import app.fedilab.android.client.mastodon.entities.Emoji; +import app.fedilab.android.client.mastodon.entities.Instance; +import app.fedilab.android.client.mastodon.entities.Tag; +import retrofit2.Call; +import retrofit2.http.GET; +import retrofit2.http.Query; + + +public interface MastodonInstanceService { + + @GET("instance") + Call instance(); + + @GET("instance/peers") + Call> connectedInstance(); + + @GET("instance/activity") + Call> weeklyActivity(); + + @GET("trends") + Call> trends(); + + @GET("directory") + Call> directory( + @Query("offset") int offset, + @Query("limit") int limit, + @Query("order") String order, + @Query("local") boolean local + ); + + @GET("custom_emojis") + Call> customEmoji(); +} diff --git a/app/src/main/java/app/fedilab/android/client/mastodon/MastodonNotificationsService.java b/app/src/main/java/app/fedilab/android/client/mastodon/MastodonNotificationsService.java new file mode 100644 index 000000000..c2ce08909 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/mastodon/MastodonNotificationsService.java @@ -0,0 +1,98 @@ +package app.fedilab.android.client.mastodon; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import java.util.List; + +import app.fedilab.android.client.mastodon.entities.Notification; +import app.fedilab.android.client.mastodon.entities.PushSubscription; +import retrofit2.Call; +import retrofit2.http.DELETE; +import retrofit2.http.Field; +import retrofit2.http.FormUrlEncoded; +import retrofit2.http.GET; +import retrofit2.http.Header; +import retrofit2.http.POST; +import retrofit2.http.PUT; +import retrofit2.http.Path; +import retrofit2.http.Query; + +public interface MastodonNotificationsService { + + @GET("notifications") + Call> getNotifications( + @Header("Authorization") String token, + @Query("exclude_types[]") List exclude_types, + @Query("account_id") String account_id, + @Query("max_id") String max_id, + @Query("since_id") String since_id, + @Query("min_id") String min_id, + @Query("limit") int limit + ); + + @GET("notifications/{id}") + Call getNotification( + @Header("Authorization") String token, + @Path("id") String id + ); + + @POST("notifications/clear") + Call clearAllNotifications( + @Header("Authorization") String token + ); + + @FormUrlEncoded + @POST("notifications/{id}/dismiss") + Call dismissNotification( + @Header("Authorization") String token, + @Path("id") String id + ); + + @FormUrlEncoded + @POST("push/subscription") + Call pushSubscription( + @Header("Authorization") String token, + @Field("subscription[endpoint]") String endpoint, + @Field("subscription[keys][p256dh]") String keys_p256dh, + @Field("subscription[keys][auth]") String keys_auth, + @Field("data[alerts][follow]") boolean follow, + @Field("data[alerts][favourite]") boolean favourite, + @Field("data[alerts][reblog]") boolean reblog, + @Field("data[alerts][mention]") boolean mention, + @Field("data[alerts][poll]") boolean poll + ); + + @GET("push/subscription") + Call getPushSubscription( + @Header("Authorization") String token + ); + + + @FormUrlEncoded + @PUT("push/subscription") + Call updatePushSubscription( + @Header("Authorization") String token, + @Field("data[alerts][follow]") boolean follow, + @Field("data[alerts][favourite]") boolean favourite, + @Field("data[alerts][reblog]") boolean reblog, + @Field("data[alerts][mention]") boolean mention, + @Field("data[alerts][poll]") boolean poll + ); + + @DELETE("push/subscription") + Call deletePushsubscription( + @Header("Authorization") String token + ); +} diff --git a/app/src/main/java/app/fedilab/android/client/mastodon/MastodonOembedService.java b/app/src/main/java/app/fedilab/android/client/mastodon/MastodonOembedService.java new file mode 100644 index 000000000..ff578ed30 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/mastodon/MastodonOembedService.java @@ -0,0 +1,31 @@ +package app.fedilab.android.client.mastodon; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + + +import app.fedilab.android.client.mastodon.entities.Oembed; +import retrofit2.Call; +import retrofit2.http.GET; +import retrofit2.http.Query; + +public interface MastodonOembedService { + + @GET("/oembed") + Call oembed( + @Query("url") String url, + @Query("maxwidth") int maxwidth, + @Query("maxheight") int maxheight + ); +} diff --git a/app/src/main/java/app/fedilab/android/client/mastodon/MastodonSearchService.java b/app/src/main/java/app/fedilab/android/client/mastodon/MastodonSearchService.java new file mode 100644 index 000000000..a8a7f1440 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/mastodon/MastodonSearchService.java @@ -0,0 +1,40 @@ +package app.fedilab.android.client.mastodon; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + + +import app.fedilab.android.client.mastodon.entities.Results; +import retrofit2.Call; +import retrofit2.http.GET; +import retrofit2.http.Header; +import retrofit2.http.Query; + +public interface MastodonSearchService { + //API V2 + @GET("search") + Call search( + @Header("Authorization") String token, + @Query("q") String q, + @Query("account_id") String account_id, + @Query("type") String type, + @Query("exclude_unreviewed") boolean exclude_unreviewed, + @Query("resolve") boolean resolve, + @Query("following") boolean following, + @Query("offset") int offset, + @Query("max_id") String max_id, + @Query("min_id") String min_id, + @Query("limit") int limit + ); +} diff --git a/app/src/main/java/app/fedilab/android/client/mastodon/MastodonStatusesService.java b/app/src/main/java/app/fedilab/android/client/mastodon/MastodonStatusesService.java new file mode 100644 index 000000000..e49037411 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/mastodon/MastodonStatusesService.java @@ -0,0 +1,284 @@ +package app.fedilab.android.client.mastodon; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import java.util.Date; +import java.util.List; + +import app.fedilab.android.client.mastodon.entities.Account; +import app.fedilab.android.client.mastodon.entities.Attachment; +import app.fedilab.android.client.mastodon.entities.Card; +import app.fedilab.android.client.mastodon.entities.Context; +import app.fedilab.android.client.mastodon.entities.Poll; +import app.fedilab.android.client.mastodon.entities.ScheduledStatus; +import app.fedilab.android.client.mastodon.entities.Status; +import okhttp3.MultipartBody; +import retrofit2.Call; +import retrofit2.http.DELETE; +import retrofit2.http.Field; +import retrofit2.http.FormUrlEncoded; +import retrofit2.http.GET; +import retrofit2.http.Header; +import retrofit2.http.Multipart; +import retrofit2.http.POST; +import retrofit2.http.PUT; +import retrofit2.http.Part; +import retrofit2.http.Path; +import retrofit2.http.Query; + + +public interface MastodonStatusesService { + + //Post a status + @FormUrlEncoded + @POST("statuses") + Call createStatus( + @Header("Idempotency-Key") String idempotency_Key, + @Header("Authorization") String token, + @Field("status") String status, + @Field("media_ids[]") List media_ids, + @Field("poll[options][]") List poll_options, + @Field("poll[expires_in]") Integer poll_expire_in, + @Field("poll[multiple]") Boolean poll_multiple, + @Field("poll[hide_totals]") Boolean poll_hide_totals, + @Field("in_reply_to_id") String in_reply_to_id, + @Field("sensitive") Boolean sensitive, + @Field("spoiler_text") String spoiler_text, + @Field("visibility") String visibility, + @Field("language") String language + ); + + //Post a scheduled status + @FormUrlEncoded + @POST("statuses") + Call createScheduledStatus( + @Header("Idempotency-Key") String idempotency_Key, + @Header("Authorization") String token, + @Field("status") String status, + @Field("media_ids[]") List media_ids, + @Field("poll[options][]") List poll_options, + @Field("poll[expires_in]") Integer poll_expire_in, + @Field("poll[multiple]") Boolean poll_multiple, + @Field("poll[hide_totals]") Boolean poll_hide_totals, + @Field("in_reply_to_id") String in_reply_to_id, + @Field("sensitive") Boolean sensitive, + @Field("spoiler_text") String spoiler_text, + @Field("visibility") String visibility, + @Field("scheduled_at") String scheduled_at, + @Field("language") String language + ); + + //Get a specific status + @GET("statuses/{id}") + Call getStatus( + @Header("Authorization") String token, + @Path("id") String id + ); + + //Delete a specific status + @DELETE("statuses/{id}") + Call deleteStatus( + @Header("Authorization") String token, + @Path("id") String id + ); + + //Get parent and child statuses + @GET("statuses/{id}/context") + Call getContext( + @Header("Authorization") String token, + @Path("id") String id + ); + + //Get reblogged by + @GET("statuses/{id}/reblogged_by") + Call> getRebloggedBy( + @Header("Authorization") String token, + @Path("id") String id, + @Query("max_id") String max_id, + @Query("since_id") String since_id, + @Query("min_id") String min_id, + @Query("limit") int limit + ); + + //Get favourited by + @GET("statuses/{id}/favourited_by") + Call> getFavourited( + @Header("Authorization") String token, + @Path("id") String id, + @Query("max_id") String max_id, + @Query("since_id") String since_id, + @Query("min_id") String min_id, + @Query("limit") int limit + ); + + //Add status to favourites + @POST("statuses/{id}/favourite") + Call favourites( + @Header("Authorization") String token, + @Path("id") String id + ); + + //Remove status from favourites + @POST("statuses/{id}/unfavourite") + Call unFavourite( + @Header("Authorization") String token, + @Path("id") String id + ); + + //Reblog a status + @FormUrlEncoded + @POST("statuses/{id}/reblog") + Call reblog( + @Header("Authorization") String token, + @Path("id") String id, + @Field("visibility") String visibility + ); + + //Unreblog a status + @POST("statuses/{id}/unreblog") + Call unReblog( + @Header("Authorization") String token, + @Path("id") String id + ); + + //Bookmark a status + @POST("statuses/{id}/bookmark") + Call bookmark( + @Header("Authorization") String token, + @Path("id") String id + ); + + //Unbookmark a status + @POST("statuses/{id}/unbookmark") + Call unBookmark( + @Header("Authorization") String token, + @Path("id") String id + ); + + //Mute a conversation + @POST("statuses/{id}/mute") + Call muteConversation( + @Header("Authorization") String token, + @Path("id") String id + ); + + //UnMute a conversation + @POST("statuses/{id}/unmute") + Call unMuteConversation( + @Header("Authorization") String token, + @Path("id") String id + ); + + //Pin a status + @POST("statuses/{id}/pin") + Call pin( + @Header("Authorization") String token, + @Path("id") String id + ); + + //UNPin a status + @POST("statuses/{id}/unpin") + Call unPin( + @Header("Authorization") String token, + @Path("id") String id + ); + + //Get reblogged by + @GET("statuses/{id}/card") + Call getCard( + @Header("Authorization") String token, + @Path("id") String id + ); + + + //Get a Media + @GET("media/{id}") + Call getMedia( + @Header("Authorization") String token, + @Path("id") String id + ); + + //Upload a Media + @Multipart + @POST("media") + Call postMedia( + @Header("Authorization") String token, + @Part MultipartBody.Part file, + @Part MultipartBody.Part thumbnail, + @Part("description") String description, + @Part("focus") String focus + ); + + //Edit a Media + @Multipart + @PUT("media/{id}") + Call updateMedia( + @Header("Authorization") String token, + @Path("id") String id, + @Part MultipartBody.Part file, + @Part MultipartBody.Part thumbnail, + @Part("description") String description, + @Part("focus") String focus + ); + + //Get a Poll + @GET("polls/{id}") + Call getPoll( + @Header("Authorization") String token, + @Path("id") String id + ); + + //Vote on a Poll + @FormUrlEncoded + @POST("polls/{id}/votes") + Call votePoll( + @Header("Authorization") String token, + @Path("id") String id, + @Field("choices[]") int[] choices + ); + + //Get scheduled statuses + @GET("scheduled_statuses") + Call> getScheduledStatuses( + @Header("Authorization") String token, + @Query("max_id") String max_id, + @Query("since_id") String since_id, + @Query("min_id") String min_id, + @Query("limit") int limit + ); + + //Get scheduled status + @GET("scheduled_statuses/{id}") + Call getScheduledStatus( + @Header("Authorization") String token, + @Path("id") String id + ); + + //Schedule a status + @FormUrlEncoded + @PUT("scheduled_statuses/{id}") + Call updateScheduleStatus( + @Header("Authorization") String token, + @Path("id") String id, + @Field("scheduled_at") Date scheduled_at + ); + + //Delete a scheduled status + @DELETE("scheduled_statuses/{id}") + Call deleteScheduledStatus( + @Header("Authorization") String token, + @Path("id") String id + ); +} diff --git a/app/src/main/java/app/fedilab/android/client/mastodon/MastodonTimelinesService.java b/app/src/main/java/app/fedilab/android/client/mastodon/MastodonTimelinesService.java new file mode 100644 index 000000000..085b2d996 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/mastodon/MastodonTimelinesService.java @@ -0,0 +1,194 @@ +package app.fedilab.android.client.mastodon; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import java.util.List; + +import app.fedilab.android.client.mastodon.entities.Account; +import app.fedilab.android.client.mastodon.entities.Conversation; +import app.fedilab.android.client.mastodon.entities.Marker; +import app.fedilab.android.client.mastodon.entities.MastodonList; +import app.fedilab.android.client.mastodon.entities.Status; +import retrofit2.Call; +import retrofit2.http.DELETE; +import retrofit2.http.Field; +import retrofit2.http.FormUrlEncoded; +import retrofit2.http.GET; +import retrofit2.http.Header; +import retrofit2.http.POST; +import retrofit2.http.PUT; +import retrofit2.http.Path; +import retrofit2.http.Query; + +public interface MastodonTimelinesService { + + //Public timelines + @GET("timelines/public") + Call> getPublic( + @Header("Authorization") String token, + @Query("local") boolean local, + @Query("remote") boolean remote, + @Query("only_media") boolean only_media, + @Query("max_id") String max_id, + @Query("since_id") String since_id, + @Query("min_id") String min_id, + @Query("limit") int limit + ); + + //Public Tags timelines + @GET("timelines/tag/{hashtag}") + Call> getHashTag( + @Header("Authorization") String token, + @Path("hashtag") String hashtag, + @Query("local") boolean local, + @Query("only_media") boolean only_media, + @Query("all[]") List all, + @Query("any[]") List any, + @Query("none[]") List none, + @Query("max_id") String max_id, + @Query("since_id") String since_id, + @Query("min_id") String min_id, + @Query("limit") int limit + ); + + //Home timeline + @GET("timelines/home") + Call> getHome( + @Header("Authorization") String token, + @Query("max_id") String max_id, + @Query("since_id") String since_id, + @Query("min_id") String min_id, + @Query("limit") int limit, + @Query("local") boolean local + ); + + //List timeline + @GET("timelines/list/{list_id}") + Call> getList( + @Header("Authorization") String token, + @Path("list_id") String list_id, + @Query("max_id") String max_id, + @Query("since_id") String since_id, + @Query("min_id") String min_id, + @Query("limit") int limit + ); + + //get conversations + @GET("conversations") + Call> getConversations( + @Header("Authorization") String token, + @Query("max_id") String max_id, + @Query("since_id") String since_id, + @Query("min_id") String min_id, + @Query("limit") int limit + ); + + //Delete a conversation + @DELETE("conversations/{id}") + Call deleteConversation( + @Header("Authorization") String token, + @Path("id") String id + ); + + //Mark a conversation as read + @FormUrlEncoded + @POST("conversations/{id}/read") + Call markReadConversation( + @Header("Authorization") String token, + @Path("id") String id + ); + + //Show user list + @GET("lists") + Call> getLists( + @Header("Authorization") String token + ); + + //Get Single list + @GET("lists/{id}") + Call getList( + @Header("Authorization") String token, + @Path("id") String id + ); + + //Create a user list + @FormUrlEncoded + @POST("lists") + Call createList( + @Header("Authorization") String token, + @Field("title") String title, + @Field("replies_policy") String replies_policy + ); + + //Update a list + @FormUrlEncoded + @PUT("lists/{id}") + Call updateList( + @Header("Authorization") String token, + @Path("id") String id, + @Field("title") String title, + @Field("replies_policy") String replies_policy + ); + + //Delete a conversation + @DELETE("lists/{id}") + Call deleteList( + @Header("Authorization") String token, + @Path("id") String id + ); + + //Get accounts in a list + @GET("lists/{id}/accounts") + Call> getAccountsInList( + @Header("Authorization") String token, + @Path("id") String id, + @Query("max_id") String max_id, + @Query("since_id") String since_id, + @Query("limit") int limit + ); + + //Add account in a list + @FormUrlEncoded + @POST("lists/{id}/accounts") + Call addAccountsList( + @Header("Authorization") String token, + @Path("id") String id, + @Field("account_ids[]") List account_ids + ); + + //Delete accounts in a list + @DELETE("lists/{id}/accounts") + Call deleteAccountsList( + @Header("Authorization") String token, + @Path("id") String id, + @Query("account_ids[]") List account_ids + ); + + //Get a marker + @GET("markers") + Call getMarker( + @Header("Authorization") String token, + @Query("timeline") List timeline + ); + + //Save marker + @FormUrlEncoded + @POST("markers") + Call addMarker( + @Header("Authorization") String token, + @Field("home[last_read_id]") String home_last_read_id, + @Field("notifications[last_read_id]") String notifications_last_read_id + ); +} diff --git a/app/src/main/java/app/fedilab/android/client/mastodon/ProgressRequestBody.java b/app/src/main/java/app/fedilab/android/client/mastodon/ProgressRequestBody.java new file mode 100644 index 000000000..3205e2c7b --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/mastodon/ProgressRequestBody.java @@ -0,0 +1,111 @@ +package app.fedilab.android.client.mastodon; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import android.os.Handler; +import android.os.Looper; + +import androidx.annotation.NonNull; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; + +import okhttp3.MediaType; +import okhttp3.RequestBody; +import okio.BufferedSink; + +public class ProgressRequestBody extends RequestBody { + + private static final int SEGMENT_SIZE = 2048; + private final File mFile; + private final String mContentType; + private final ProgressListener mListener; + private final long mTotalToUpload; + private long mLastTotalUploaded; + + public ProgressRequestBody(File file, String content_type, long lastTotalUploaded, long totalToUpload, ProgressListener listener) { + mFile = file; + mContentType = content_type; + mListener = listener; + mTotalToUpload = totalToUpload; + mLastTotalUploaded = lastTotalUploaded; + } + + @Override + public MediaType contentType() { + return MediaType.parse(mContentType); + } + + @Override + public long contentLength() { + return mFile.length(); + } + + @Override + public void writeTo(@NonNull BufferedSink sink) throws IOException { + byte[] buffer = new byte[SEGMENT_SIZE]; + FileInputStream in = new FileInputStream(mFile); + try { + int read; + Handler handler = new Handler(Looper.getMainLooper()); + while ((read = in.read(buffer)) != -1) { + // update progress on UI thread + handler.post(new ProgressUpdater(mLastTotalUploaded, mTotalToUpload)); + mLastTotalUploaded += read; + sink.write(buffer, 0, read); + } + } catch (Exception e) { + e.printStackTrace(); + } finally { + in.close(); + } + /* Source source = null; + try { + source = Okio.source(mFile); + + long read; + + while ((read = source.read(bufferedSink.getBuffer(), SEGMENT_SIZE)) != -1) { + mLastTotalUploaded += read; + bufferedSink.flush(); + mListener.onProgressUpdate((int)(100 * mLastTotalUploaded / mTotalToUpload)); + + } + } finally { + Util.closeQuietly(source); + }*/ + } + + public interface ProgressListener { + void onProgressUpdate(int percentage); + } + + private class ProgressUpdater implements Runnable { + private final long mUploaded; + private final long mTotal; + + public ProgressUpdater(long uploaded, long total) { + mUploaded = uploaded; + mTotal = total; + } + + @Override + public void run() { + mListener.onProgressUpdate((int) (100 * mUploaded / mTotal)); + } + } +} + diff --git a/app/src/main/java/app/fedilab/android/client/mastodon/entities/Account.java b/app/src/main/java/app/fedilab/android/client/mastodon/entities/Account.java new file mode 100644 index 000000000..bdc530043 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/mastodon/entities/Account.java @@ -0,0 +1,101 @@ +package app.fedilab.android.client.mastodon.entities; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import android.text.Spannable; + +import com.google.gson.annotations.SerializedName; + +import java.io.Serializable; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.List; + +public class Account implements Serializable { + + @SerializedName("id") + public String id; + @SerializedName("username") + public String username; + @SerializedName("acct") + public String acct; + @SerializedName("display_name") + public String display_name; + @SerializedName("locked") + public boolean locked; + @SerializedName("bot") + public boolean bot; + @SerializedName("created_at") + public Date created_at; + @SerializedName("note") + public String note; + @SerializedName("url") + public String url; + @SerializedName("avatar") + public String avatar; + @SerializedName("avatar_static") + public String avatar_static; + @SerializedName("header") + public String header; + @SerializedName("header_static") + public String header_static; + @SerializedName("followers_count") + public int followers_count; + @SerializedName("following_count") + public int following_count; + @SerializedName("statuses_count") + public int statuses_count; + @SerializedName("last_status_at") + public Date last_status_at; + @SerializedName("source") + public Source source; + @SerializedName("emojis") + public List emojis; + @SerializedName("fields") + public List fields; + @SerializedName("suspended") + public boolean suspended; + @SerializedName("discoverable") + public boolean discoverable; + @SerializedName("mute_expires_at") + public Date mute_expires_at; + @SerializedName("moved") + public Account moved; + + + //Some extra spannable element - They will be filled automatically when fetching the account + public transient Spannable span_display_name; + public transient Spannable span_note; + public transient RelationShip relationShip; + + + public static class AccountParams implements Serializable { + @SerializedName("discoverable") + public boolean discoverable; + @SerializedName("bot") + public boolean bot; + @SerializedName("display_name") + public String display_name; + @SerializedName("note") + public String note; + @SerializedName("locked") + public boolean locked; + @SerializedName("source") + public Source.SourceParams source; + @SerializedName("fields_attributes") + public LinkedHashMap fields; + + } +} diff --git a/app/src/main/java/app/fedilab/android/client/mastodon/entities/Accounts.java b/app/src/main/java/app/fedilab/android/client/mastodon/entities/Accounts.java new file mode 100644 index 000000000..73b5253ef --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/mastodon/entities/Accounts.java @@ -0,0 +1,22 @@ +package app.fedilab.android.client.mastodon.entities; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import java.util.List; + +public class Accounts { + public Pagination pagination = new Pagination(); + public List accounts; +} diff --git a/app/src/main/java/app/fedilab/android/client/mastodon/entities/Activity.java b/app/src/main/java/app/fedilab/android/client/mastodon/entities/Activity.java new file mode 100644 index 000000000..3659f57ad --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/mastodon/entities/Activity.java @@ -0,0 +1,28 @@ +package app.fedilab.android.client.mastodon.entities; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import com.google.gson.annotations.SerializedName; + +public class Activity { + @SerializedName("week") + public String week; + @SerializedName("statuses") + public String statuses; + @SerializedName("logins") + public String logins; + @SerializedName("registrations") + public String registrations; +} diff --git a/app/src/main/java/app/fedilab/android/client/mastodon/entities/AdminAccount.java b/app/src/main/java/app/fedilab/android/client/mastodon/entities/AdminAccount.java new file mode 100644 index 000000000..1741fd34d --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/mastodon/entities/AdminAccount.java @@ -0,0 +1,57 @@ +package app.fedilab.android.client.mastodon.entities; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import com.google.gson.annotations.SerializedName; + +import java.util.Date; + +public class AdminAccount { + + @SerializedName("id") + public String id; + @SerializedName("username") + public String username; + @SerializedName("domain") + public String domain; + @SerializedName("created_at") + public Date created_at; + @SerializedName("email") + public String email; + @SerializedName("ip") + public String ip; + @SerializedName("locale") + public String locale; + @SerializedName("invite_request") + public String invite_request; + @SerializedName("role") + public String role; + @SerializedName("confirmed") + public boolean confirmed; + @SerializedName("approved") + public boolean approved; + @SerializedName("disabled") + public boolean disabled; + @SerializedName("silenced") + public boolean silenced; + @SerializedName("suspended") + public boolean suspended; + @SerializedName("account") + public Account account; + @SerializedName("created_by_application_id") + public String created_by_application_id; + @SerializedName("invited_by_account_id") + public String invited_by_account_id; +} diff --git a/app/src/main/java/app/fedilab/android/client/mastodon/entities/AdminReport.java b/app/src/main/java/app/fedilab/android/client/mastodon/entities/AdminReport.java new file mode 100644 index 000000000..5611ae5d8 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/mastodon/entities/AdminReport.java @@ -0,0 +1,44 @@ +package app.fedilab.android.client.mastodon.entities; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import com.google.gson.annotations.SerializedName; + +import java.util.Date; +import java.util.List; + +public class AdminReport { + + @SerializedName("id") + public String id; + @SerializedName("action_taken") + public String action_taken; + @SerializedName("comment") + public String comment; + @SerializedName("created_at") + public Date created_at; + @SerializedName("updated_at") + public Date updated_at; + @SerializedName("account") + public Account account; + @SerializedName("target_account") + public Account target_account; + @SerializedName("assigned_account") + public Account assigned_account; + @SerializedName("action_taken_by_account") + public String action_taken_by_account; + @SerializedName("statuses") + public List statuses; +} diff --git a/app/src/main/java/app/fedilab/android/client/mastodon/entities/Announcement.java b/app/src/main/java/app/fedilab/android/client/mastodon/entities/Announcement.java new file mode 100644 index 000000000..b7dffec6a --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/mastodon/entities/Announcement.java @@ -0,0 +1,49 @@ +package app.fedilab.android.client.mastodon.entities; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import com.google.gson.annotations.SerializedName; + +import java.util.Date; +import java.util.List; + +public class Announcement { + @SerializedName("id") + public String id; + @SerializedName("content") + public String content; + @SerializedName("starts_at") + public Date starts_at; + @SerializedName("ends_at") + public Date ends_at; + @SerializedName("all_day") + public boolean all_day; + @SerializedName("published_at") + public Date published_at; + @SerializedName("updated_at") + public Date updated_at; + @SerializedName("read") + public boolean read; + @SerializedName("mentions") + public List mentions; + @SerializedName("statuses") + public List statuses; + @SerializedName("tags") + public List tags; + @SerializedName("emojis") + public List emojis; + @SerializedName("reactions") + public List reactions; +} diff --git a/app/src/main/java/app/fedilab/android/client/mastodon/entities/App.java b/app/src/main/java/app/fedilab/android/client/mastodon/entities/App.java new file mode 100644 index 000000000..abf1a190e --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/mastodon/entities/App.java @@ -0,0 +1,37 @@ +package app.fedilab.android.client.mastodon.entities; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import com.google.gson.annotations.SerializedName; + +import java.io.Serializable; + +public class App implements Serializable { + + @SerializedName("id") + public String id; + @SerializedName("name") + public String name; + @SerializedName("website") + public String website; + @SerializedName("redirect_uri") + public String redirect_uri; + @SerializedName("client_id") + public String client_id; + @SerializedName("client_secret") + public String client_secret; + @SerializedName("vapid_key") + public String vapid_key; +} diff --git a/app/src/main/java/app/fedilab/android/client/mastodon/entities/Attachment.java b/app/src/main/java/app/fedilab/android/client/mastodon/entities/Attachment.java new file mode 100644 index 000000000..b07ee6f65 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/mastodon/entities/Attachment.java @@ -0,0 +1,47 @@ +package app.fedilab.android.client.mastodon.entities; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import com.google.gson.annotations.SerializedName; + +import java.io.Serializable; + +public class Attachment implements Serializable { + @SerializedName("id") + public String id; + @SerializedName("type") + public String type; + @SerializedName("url") + public String url; + @SerializedName("preview_url") + public String preview_url; + @SerializedName("remote_url") + public String remote_url; + @SerializedName("text_url") + public String text_url; + @SerializedName("description") + public String description; + @SerializedName("blurhash") + public String blurhash; + @SerializedName("mimeType") + public String mimeType; + @SerializedName("filename") + public String filename; + @SerializedName("size") + public long size; + @SerializedName("local_path") + public String local_path; + +} diff --git a/app/src/main/java/app/fedilab/android/client/mastodon/entities/Card.java b/app/src/main/java/app/fedilab/android/client/mastodon/entities/Card.java new file mode 100644 index 000000000..4f69fa6f4 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/mastodon/entities/Card.java @@ -0,0 +1,50 @@ +package app.fedilab.android.client.mastodon.entities; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import com.google.gson.annotations.SerializedName; + +import java.io.Serializable; + +public class Card implements Serializable { + @SerializedName("url") + public String url; + @SerializedName("title") + public String title; + @SerializedName("description") + public String description; + @SerializedName("type") + public String type; + @SerializedName("author_name") + public String author_name; + @SerializedName("author_url") + public String author_url; + @SerializedName("provider_name") + public String provider_name; + @SerializedName("provider_url") + public String provider_url; + @SerializedName("html") + public String html; + @SerializedName("width") + public int width; + @SerializedName("height") + public int height; + @SerializedName("image") + public String image; + @SerializedName("embed_url") + public String embed_url; + @SerializedName("blurhash") + public String blurhash; +} diff --git a/app/src/main/java/app/fedilab/android/client/mastodon/entities/Context.java b/app/src/main/java/app/fedilab/android/client/mastodon/entities/Context.java new file mode 100644 index 000000000..017961d11 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/mastodon/entities/Context.java @@ -0,0 +1,27 @@ +package app.fedilab.android.client.mastodon.entities; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import com.google.gson.annotations.SerializedName; + +import java.util.List; + +public class Context { + @SerializedName("ancestors") + public List ancestors; + @SerializedName("descendants") + public List descendants; + +} diff --git a/app/src/main/java/app/fedilab/android/client/mastodon/entities/Conversation.java b/app/src/main/java/app/fedilab/android/client/mastodon/entities/Conversation.java new file mode 100644 index 000000000..b155b4c5d --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/mastodon/entities/Conversation.java @@ -0,0 +1,30 @@ +package app.fedilab.android.client.mastodon.entities; + +import com.google.gson.annotations.SerializedName; + +import java.util.List; + +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ +public class Conversation { + @SerializedName("id") + public String id; + @SerializedName("unread") + public boolean unread; + @SerializedName("accounts") + public List accounts; + @SerializedName("last_status") + public Status last_status; +} diff --git a/app/src/main/java/app/fedilab/android/client/mastodon/entities/Conversations.java b/app/src/main/java/app/fedilab/android/client/mastodon/entities/Conversations.java new file mode 100644 index 000000000..cc559b75a --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/mastodon/entities/Conversations.java @@ -0,0 +1,22 @@ +package app.fedilab.android.client.mastodon.entities; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import java.util.List; + +public class Conversations { + public Pagination pagination = new Pagination(); + public List conversations; +} diff --git a/app/src/main/java/app/fedilab/android/client/mastodon/entities/Emoji.java b/app/src/main/java/app/fedilab/android/client/mastodon/entities/Emoji.java new file mode 100644 index 000000000..a059f0e0b --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/mastodon/entities/Emoji.java @@ -0,0 +1,32 @@ +package app.fedilab.android.client.mastodon.entities; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import com.google.gson.annotations.SerializedName; + +import java.io.Serializable; + +public class Emoji implements Serializable { + @SerializedName("shortcode") + public String shortcode; + @SerializedName("url") + public String url; + @SerializedName("static_url") + public String static_url; + @SerializedName("visible_in_picker") + public boolean visible_in_picker; + @SerializedName("category") + public String category; +} diff --git a/app/src/main/java/app/fedilab/android/client/mastodon/entities/EmojiInstance.java b/app/src/main/java/app/fedilab/android/client/mastodon/entities/EmojiInstance.java new file mode 100644 index 000000000..dd3a587bd --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/mastodon/entities/EmojiInstance.java @@ -0,0 +1,217 @@ +package app.fedilab.android.client.mastodon.entities; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; + +import com.google.gson.Gson; +import com.google.gson.annotations.SerializedName; +import com.google.gson.reflect.TypeToken; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +import app.fedilab.android.exception.DBException; +import app.fedilab.android.sqlite.Sqlite; + + +public class EmojiInstance implements Serializable { + private final SQLiteDatabase db; + @SerializedName("instance") + public String instance; + @SerializedName("emojiList") + public List emojiList; + private Context context; + + public EmojiInstance() { + db = null; + } + + public EmojiInstance(Context context) { + //Creation of the DB with tables + this.context = context; + this.db = Sqlite.getInstance(context.getApplicationContext(), Sqlite.DB_NAME, null, Sqlite.DB_VERSION).open(); + } + + /** + * Serialized a list of Emoji class + * + * @param emojis List of {@link Emoji} to serialize + * @return String serialized emoji list + */ + public static String mastodonEmojiListToStringStorage(List emojis) { + Gson gson = new Gson(); + try { + return gson.toJson(emojis); + } catch (Exception e) { + return null; + } + } + + /** + * Unserialized a Emoji List + * + * @param serializedEmojiList String serialized account + * @return List of {@link Emoji} + */ + public static List restoreEmojiListFromString(String serializedEmojiList) { + Gson gson = new Gson(); + try { + return gson.fromJson(serializedEmojiList, new TypeToken>() { + }.getType()); + } catch (Exception e) { + return null; + } + } + + /** + * Insert or update emoji + * + * @param emojiInstance {@link EmojiInstance} + * @return long - db id + * @throws DBException exception with database + */ + public long insertOrUpdate(EmojiInstance emojiInstance) throws DBException { + if (db == null) { + throw new DBException("db is null. Wrong initialization."); + } + if (emojiInstance == null) { + return -1; + } + boolean exists = emojiInstanceExist(emojiInstance); + long idReturned; + if (exists) { + idReturned = updateEmojiInstance(emojiInstance); + } else { + idReturned = insertEmojiInstance(emojiInstance); + } + return idReturned; + } + + /** + * Check if emojis exists in db + * + * @param emojiInstance EmojiInstance {@link EmojiInstance} + * @return boolean - emojiInstance exists + * @throws DBException Exception + */ + public boolean emojiInstanceExist(EmojiInstance emojiInstance) throws DBException { + if (db == null) { + throw new DBException("db is null. Wrong initialization."); + } + Cursor mCount = db.rawQuery("select count(*) from " + Sqlite.TABLE_EMOJI_INSTANCE + + " where " + Sqlite.COL_INSTANCE + " = '" + emojiInstance.instance + "'", null); + mCount.moveToFirst(); + int count = mCount.getInt(0); + mCount.close(); + return (count > 0); + } + + /** + * Insert emojis in db + * + * @param emojiInstance {@link EmojiInstance} + * @return long - db id + * @throws DBException exception with database + */ + private long insertEmojiInstance(EmojiInstance emojiInstance) throws DBException { + if (db == null) { + throw new DBException("db is null. Wrong initialization."); + } + ContentValues values = new ContentValues(); + values.put(Sqlite.COL_INSTANCE, emojiInstance.instance); + values.put(Sqlite.COL_EMOJI_LIST, mastodonEmojiListToStringStorage(emojiInstance.emojiList)); + //Inserts token + try { + return db.insertOrThrow(Sqlite.TABLE_EMOJI_INSTANCE, null, values); + } catch (Exception e) { + e.printStackTrace(); + return -1; + } + } + + /** + * update emojis in db + * + * @param emojiInstance {@link EmojiInstance} + * @return long - db id + * @throws DBException exception with database + */ + private long updateEmojiInstance(EmojiInstance emojiInstance) throws DBException { + if (db == null) { + throw new DBException("db is null. Wrong initialization."); + } + ContentValues values = new ContentValues(); + values.put(Sqlite.COL_EMOJI_LIST, mastodonEmojiListToStringStorage(emojiInstance.emojiList)); + //Inserts token + try { + return db.update(Sqlite.TABLE_EMOJI_INSTANCE, + values, Sqlite.COL_INSTANCE + " = ?", + new String[]{emojiInstance.instance}); + } catch (Exception e) { + e.printStackTrace(); + return -1; + } + } + + /** + * Returns the emojis for an instance + * + * @param instance String + * @return List - List of {@link Emoji} + */ + public List getEmojiList(String instance) throws DBException { + if (db == null) { + throw new DBException("db is null. Wrong initialization."); + } + try { + Cursor c = db.query(Sqlite.TABLE_EMOJI_INSTANCE, null, Sqlite.COL_INSTANCE + " = '" + instance + "'", null, null, null, null, "1"); + return cursorToEmojiList(c); + } catch (Exception e) { + return null; + } + } + + /** + * Restore emoji list from db + * + * @param c Cursor + * @return List + */ + private List cursorToEmojiList(Cursor c) { + //No element found + if (c.getCount() == 0) { + c.close(); + return null; + } + //Take the first element + c.moveToFirst(); + List emojiList = restoreEmojiListFromString(c.getString(c.getColumnIndexOrThrow(Sqlite.COL_EMOJI_LIST))); + c.close(); + List filteredEmojis = new ArrayList<>(); + if (emojiList != null) { + for (Emoji emoji : emojiList) { + if (emoji.visible_in_picker) { + filteredEmojis.add(emoji); + } + } + } + return filteredEmojis; + } +} diff --git a/app/src/main/java/app/fedilab/android/client/mastodon/entities/Error.java b/app/src/main/java/app/fedilab/android/client/mastodon/entities/Error.java new file mode 100644 index 000000000..bacdc0b7d --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/mastodon/entities/Error.java @@ -0,0 +1,27 @@ +package app.fedilab.android.client.mastodon.entities; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import com.google.gson.annotations.SerializedName; + +public class Error { + @SerializedName("code") + public int code; + @SerializedName("error") + public String error; + @SerializedName("error_description") + public String error_description; + +} diff --git a/app/src/main/java/app/fedilab/android/client/mastodon/entities/FeaturedTag.java b/app/src/main/java/app/fedilab/android/client/mastodon/entities/FeaturedTag.java new file mode 100644 index 000000000..f8e43418e --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/mastodon/entities/FeaturedTag.java @@ -0,0 +1,32 @@ +package app.fedilab.android.client.mastodon.entities; + +import com.google.gson.annotations.SerializedName; + +import java.util.Date; + +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ +public class FeaturedTag { + @SerializedName("id") + public String id; + @SerializedName("name") + public String name; + @SerializedName("url") + public String url; + @SerializedName("statuses_count") + public int statuses_count; + @SerializedName("last_status_at") + public Date last_status_at; +} diff --git a/app/src/main/java/app/fedilab/android/client/mastodon/entities/Field.java b/app/src/main/java/app/fedilab/android/client/mastodon/entities/Field.java new file mode 100644 index 000000000..a60be9124 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/mastodon/entities/Field.java @@ -0,0 +1,41 @@ +package app.fedilab.android.client.mastodon.entities; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import android.text.Spannable; + +import com.google.gson.annotations.SerializedName; + +import java.io.Serializable; +import java.util.Date; + +public class Field implements Serializable { + @SerializedName("name") + public String name; + @SerializedName("value") + public String value; + @SerializedName("verified_at") + public Date verified_at; + + //Some extra spannable element - They will be filled automatically when fetching the account + public transient Spannable value_span; + + public static class FieldParams implements Serializable { + @SerializedName("name") + public String name; + @SerializedName("value") + public String value; + } +} diff --git a/app/src/main/java/app/fedilab/android/client/mastodon/entities/Filter.java b/app/src/main/java/app/fedilab/android/client/mastodon/entities/Filter.java new file mode 100644 index 000000000..2a8894e57 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/mastodon/entities/Filter.java @@ -0,0 +1,38 @@ +package app.fedilab.android.client.mastodon.entities; + +import com.google.gson.annotations.SerializedName; + +import java.util.Date; +import java.util.List; + +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +public class Filter { + @SerializedName("id") + public String id; + @SerializedName("phrase") + public String phrase; + @SerializedName("context") + public List context; + @SerializedName("whole_word") + public boolean whole_word; + @SerializedName("expires_at") + public Date expires_at; + @SerializedName("expires_at_sent") + public long expires_at_sent; + @SerializedName("irreversible") + public boolean irreversible; +} diff --git a/app/src/main/java/app/fedilab/android/client/mastodon/entities/History.java b/app/src/main/java/app/fedilab/android/client/mastodon/entities/History.java new file mode 100644 index 000000000..ff9d693a8 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/mastodon/entities/History.java @@ -0,0 +1,26 @@ +package app.fedilab.android.client.mastodon.entities; + +import com.google.gson.annotations.SerializedName; + +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ +public class History { + @SerializedName("day") + public String day; + @SerializedName("uses") + public String uses; + @SerializedName("accounts") + public String accounts; +} diff --git a/app/src/main/java/app/fedilab/android/client/mastodon/entities/IdentityProof.java b/app/src/main/java/app/fedilab/android/client/mastodon/entities/IdentityProof.java new file mode 100644 index 000000000..66c4d3be7 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/mastodon/entities/IdentityProof.java @@ -0,0 +1,32 @@ +package app.fedilab.android.client.mastodon.entities; + +import com.google.gson.annotations.SerializedName; + +import java.util.Date; + +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ +public class IdentityProof { + @SerializedName("provider") + public String provider; + @SerializedName("provider_username") + public String provider_username; + @SerializedName("updated_at") + public Date updated_at; + @SerializedName("proof_url") + public String proof_url; + @SerializedName("profile_url") + public String profile_url; +} diff --git a/app/src/main/java/app/fedilab/android/client/mastodon/entities/Instance.java b/app/src/main/java/app/fedilab/android/client/mastodon/entities/Instance.java new file mode 100644 index 000000000..58eb294c6 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/mastodon/entities/Instance.java @@ -0,0 +1,178 @@ +package app.fedilab.android.client.mastodon.entities; + +import com.google.gson.annotations.SerializedName; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +public class Instance implements Serializable { + + @SerializedName("uri") + public String uri; + @SerializedName("title") + public String title; + @SerializedName("short_description") + public String short_description; + @SerializedName("description") + public String description; + @SerializedName("email") + public String email; + @SerializedName("version") + public String version; + @SerializedName("languages") + public List languages; + @SerializedName("registrations") + public boolean registrations; + @SerializedName("rules") + public List rules; + @SerializedName("approval_required") + public boolean approval_required; + @SerializedName("invites_enabled") + public boolean invites_enabled; + @SerializedName("stats") + public Stats stats; + @SerializedName("urls") + public Urls urls; + @SerializedName("thumbnail") + public String thumbnail; + @SerializedName("contact_account") + public Account contact_account; + @SerializedName("configuration") + public Configuration configuration; + + + public List getMimeTypeAudio() { + List mimeTypes = new ArrayList<>(); + if (configuration == null || configuration.media_attachments == null) { + return mimeTypes; + } + for (String mimeType : configuration.media_attachments.supported_mime_types) { + if (mimeType.startsWith("audio")) { + mimeTypes.add(mimeType); + } + } + return mimeTypes; + } + + public List getMimeTypeVideo() { + List mimeTypes = new ArrayList<>(); + if (configuration == null || configuration.media_attachments == null) { + return mimeTypes; + } + for (String mimeType : configuration.media_attachments.supported_mime_types) { + if (mimeType.startsWith("video")) { + mimeTypes.add(mimeType); + } + } + return mimeTypes; + } + + + public List getMimeTypeImage() { + List mimeTypes = new ArrayList<>(); + if (configuration == null || configuration.media_attachments == null) { + return mimeTypes; + } + for (String mimeType : configuration.media_attachments.supported_mime_types) { + if (mimeType.startsWith("image")) { + mimeTypes.add(mimeType); + } + } + return mimeTypes; + } + + public List getMimeTypeOther() { + List mimeTypes = new ArrayList<>(); + if (configuration == null || configuration.media_attachments == null) { + return mimeTypes; + } + for (String mimeType : configuration.media_attachments.supported_mime_types) { + if (!mimeType.startsWith("image") && !mimeType.startsWith("video") && !mimeType.startsWith("audio")) { + mimeTypes.add(mimeType); + } + } + return mimeTypes; + } + + public static class Configuration implements Serializable { + @SerializedName("statuses") + public StatusesConf statusesConf; + @SerializedName("polls") + public PollsConf pollsConf; + @SerializedName("media_attachments") + public MediaConf media_attachments; + } + + public static class StatusesConf implements Serializable { + @SerializedName("max_characters") + public int max_characters = 500; + @SerializedName("max_media_attachments") + public int max_media_attachments = 4; + @SerializedName("characters_reserved_per_url") + public int characters_reserved_per_url; + } + + public static class MediaConf implements Serializable { + @SerializedName("supported_mime_types") + public List supported_mime_types; + @SerializedName("image_size_limit") + public int image_size_limit; + @SerializedName("image_matrix_limit") + public int image_matrix_limit; + @SerializedName("video_size_limit") + public int video_size_limit; + @SerializedName("video_frame_rate_limit") + public int video_frame_rate_limit; + @SerializedName("video_matrix_limit") + public int video_matrix_limit; + } + + public static class PollsConf implements Serializable { + @SerializedName("min_expiration") + public int min_expiration; + @SerializedName("max_options") + public int max_options = 4; + @SerializedName("max_option_chars") + public int max_option_chars = 25; + @SerializedName("max_expiration") + public int max_expiration; + } + + public static class Stats implements Serializable { + @SerializedName("user_count") + public int user_count; + @SerializedName("status_count") + public int status_count; + @SerializedName("domain_count") + public int domain_count; + } + + public static class Urls implements Serializable { + @SerializedName("streaming_api") + public String streaming_api; + } + + public static class Rule implements Serializable { + @SerializedName("id") + public String id; + @SerializedName("text") + public String text; + public transient boolean isChecked = false; + } +} diff --git a/app/src/main/java/app/fedilab/android/client/mastodon/entities/InstanceInfo.java b/app/src/main/java/app/fedilab/android/client/mastodon/entities/InstanceInfo.java new file mode 100644 index 000000000..f7eddcbf7 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/mastodon/entities/InstanceInfo.java @@ -0,0 +1,204 @@ +package app.fedilab.android.client.mastodon.entities; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; + +import com.google.gson.Gson; +import com.google.gson.annotations.SerializedName; + +import java.io.Serializable; + +import app.fedilab.android.exception.DBException; +import app.fedilab.android.sqlite.Sqlite; + + +public class InstanceInfo implements Serializable { + + private final SQLiteDatabase db; + @SerializedName("instance") + public String instance; + @SerializedName("info") + public Instance info; + + public InstanceInfo() { + db = null; + } + + public InstanceInfo(Context context) { + //Creation of the DB with tables + this.db = Sqlite.getInstance(context.getApplicationContext(), Sqlite.DB_NAME, null, Sqlite.DB_VERSION).open(); + } + + /** + * Serialized a list of Emoji class + * + * @param instance {@link Instance} to serialize + * @return String serialized instance + */ + public static String instanceInfoToStringStorage(Instance instance) { + Gson gson = new Gson(); + try { + return gson.toJson(instance); + } catch (Exception e) { + return null; + } + } + + /** + * Unserialized an instance + * + * @param serializedInstanceInfo String serialized instance + * @return {@link Instance} + */ + public static Instance restoreInstanceInfoFromString(String serializedInstanceInfo) { + Gson gson = new Gson(); + try { + return gson.fromJson(serializedInstanceInfo, Instance.class); + } catch (Exception e) { + return null; + } + } + + /** + * Insert or update instance + * + * @param instanceInfo {@link InstanceInfo} + * @return long - db id + * @throws DBException exception with database + */ + public long insertOrUpdate(InstanceInfo instanceInfo) throws DBException { + if (db == null) { + throw new DBException("db is null. Wrong initialization."); + } + if (instanceInfo == null) { + return -1; + } + boolean exists = instanceInfoExist(instanceInfo); + long idReturned; + if (exists) { + idReturned = updateInstanceInfo(instanceInfo); + } else { + idReturned = insertInstanceInfo(instanceInfo); + } + return idReturned; + } + + /** + * Check if instanceInfo exists in db + * + * @param instanceInfo InstanceInfo {@link InstanceInfo} + * @return boolean - instanceInfo exists + * @throws DBException Exception + */ + public boolean instanceInfoExist(InstanceInfo instanceInfo) throws DBException { + if (db == null) { + throw new DBException("db is null. Wrong initialization."); + } + Cursor mCount = db.rawQuery("select count(*) from " + Sqlite.TABLE_INSTANCE_INFO + + " where " + Sqlite.COL_INSTANCE + " = '" + instanceInfo.instance + "'", null); + mCount.moveToFirst(); + int count = mCount.getInt(0); + mCount.close(); + return (count > 0); + } + + /** + * Insert instanceInfo in db + * + * @param instanceInfo {@link InstanceInfo} + * @return long - db id + * @throws DBException exception with database + */ + private long insertInstanceInfo(InstanceInfo instanceInfo) throws DBException { + if (db == null) { + throw new DBException("db is null. Wrong initialization."); + } + ContentValues values = new ContentValues(); + values.put(Sqlite.COL_INSTANCE, instanceInfo.instance); + values.put(Sqlite.COL_INFO, instanceInfoToStringStorage(instanceInfo.info)); + //Inserts instance + try { + return db.insertOrThrow(Sqlite.TABLE_INSTANCE_INFO, null, values); + } catch (Exception e) { + e.printStackTrace(); + return -1; + } + } + + /** + * update instanceInfo in db + * + * @param instanceInfo {@link InstanceInfo} + * @return long - db id + * @throws DBException exception with database + */ + private long updateInstanceInfo(InstanceInfo instanceInfo) throws DBException { + if (db == null) { + throw new DBException("db is null. Wrong initialization."); + } + ContentValues values = new ContentValues(); + values.put(Sqlite.COL_INFO, instanceInfoToStringStorage(instanceInfo.info)); + //Inserts token + try { + return db.update(Sqlite.TABLE_INSTANCE_INFO, + values, Sqlite.COL_INSTANCE + " = ?", + new String[]{instanceInfo.instance}); + } catch (Exception e) { + e.printStackTrace(); + return -1; + } + } + + /** + * Returns the info for an instance + * + * @param instance String + * @return InstanceInfo - {@link InstanceInfo} + */ + public Instance getInstanceInfo(String instance) throws DBException { + if (db == null) { + throw new DBException("db is null. Wrong initialization."); + } + try { + Cursor c = db.query(Sqlite.TABLE_INSTANCE_INFO, null, Sqlite.COL_INSTANCE + " = '" + instance + "'", null, null, null, null, "1"); + return cursorToInstanceInfo(c); + } catch (Exception e) { + return null; + } + } + + /** + * Restore instanceInfo from db + * + * @param c Cursor + * @return Instance + */ + private Instance cursorToInstanceInfo(Cursor c) { + //No element found + if (c.getCount() == 0) { + c.close(); + return null; + } + //Take the first element + c.moveToFirst(); + Instance instance = restoreInstanceInfoFromString(c.getString(c.getColumnIndexOrThrow(Sqlite.COL_INFO))); + c.close(); + return instance; + } +} diff --git a/app/src/main/java/app/fedilab/android/client/mastodon/entities/JoinMastodonInstance.java b/app/src/main/java/app/fedilab/android/client/mastodon/entities/JoinMastodonInstance.java new file mode 100644 index 000000000..b6e59b28b --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/mastodon/entities/JoinMastodonInstance.java @@ -0,0 +1,44 @@ +package app.fedilab.android.client.mastodon.entities; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import com.google.gson.annotations.SerializedName; + +import java.util.List; + +public class JoinMastodonInstance { + @SerializedName("domain") + public String domain; + @SerializedName("version") + public String version; + @SerializedName("description") + public String description; + @SerializedName("languages") + public List languages; + @SerializedName("categories") + public List categories; + @SerializedName("proxied_thumbnail") + public String proxied_thumbnail; + @SerializedName("total_users") + public int total_users; + @SerializedName("last_week_users") + public int last_week_users; + @SerializedName("approval_required") + public boolean approval_required; + @SerializedName("language") + public String language; + @SerializedName("general") + public String general; +} diff --git a/app/src/main/java/app/fedilab/android/client/mastodon/entities/Marker.java b/app/src/main/java/app/fedilab/android/client/mastodon/entities/Marker.java new file mode 100644 index 000000000..2b37e6889 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/mastodon/entities/Marker.java @@ -0,0 +1,37 @@ +package app.fedilab.android.client.mastodon.entities; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import com.google.gson.annotations.SerializedName; + +import java.util.Date; + +public class Marker { + + @SerializedName("home") + public MarkerContent home; + @SerializedName("notifications") + public MarkerContent notifications; + + + public static class MarkerContent { + @SerializedName("last_read_id") + public String last_read_id; + @SerializedName("version") + public int version; + @SerializedName("updated_at") + public Date updated_at; + } +} diff --git a/app/src/main/java/app/fedilab/android/client/mastodon/entities/MastodonList.java b/app/src/main/java/app/fedilab/android/client/mastodon/entities/MastodonList.java new file mode 100644 index 000000000..714a0e534 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/mastodon/entities/MastodonList.java @@ -0,0 +1,29 @@ +package app.fedilab.android.client.mastodon.entities; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import com.google.gson.annotations.SerializedName; + +import java.io.Serializable; + +public class MastodonList implements Serializable { + + @SerializedName("id") + public String id; + @SerializedName("title") + public String title; + @SerializedName("replies_policy") + public String replies_policy; +} diff --git a/app/src/main/java/app/fedilab/android/client/mastodon/entities/Mention.java b/app/src/main/java/app/fedilab/android/client/mastodon/entities/Mention.java new file mode 100644 index 000000000..714ee3d10 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/mastodon/entities/Mention.java @@ -0,0 +1,31 @@ +package app.fedilab.android.client.mastodon.entities; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import com.google.gson.annotations.SerializedName; + +import java.io.Serializable; + +public class Mention implements Serializable { + + @SerializedName("id") + public String id; + @SerializedName("username") + public String username; + @SerializedName("url") + public String url; + @SerializedName("acct") + public String acct; +} diff --git a/app/src/main/java/app/fedilab/android/client/mastodon/entities/Notification.java b/app/src/main/java/app/fedilab/android/client/mastodon/entities/Notification.java new file mode 100644 index 000000000..9d63b1b4e --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/mastodon/entities/Notification.java @@ -0,0 +1,33 @@ +package app.fedilab.android.client.mastodon.entities; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import com.google.gson.annotations.SerializedName; + +import java.util.Date; + +public class Notification { + + @SerializedName("id") + public String id; + @SerializedName("type") + public String type; + @SerializedName("created_at") + public Date created_at; + @SerializedName("account") + public Account account; + @SerializedName("status") + public Status status; +} diff --git a/app/src/main/java/app/fedilab/android/client/mastodon/entities/Notifications.java b/app/src/main/java/app/fedilab/android/client/mastodon/entities/Notifications.java new file mode 100644 index 000000000..cd3b3ac8e --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/mastodon/entities/Notifications.java @@ -0,0 +1,22 @@ +package app.fedilab.android.client.mastodon.entities; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import java.util.List; + +public class Notifications { + public Pagination pagination = new Pagination(); + public List notifications; +} diff --git a/app/src/main/java/app/fedilab/android/client/mastodon/entities/Oembed.java b/app/src/main/java/app/fedilab/android/client/mastodon/entities/Oembed.java new file mode 100644 index 000000000..7d465ac7f --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/mastodon/entities/Oembed.java @@ -0,0 +1,44 @@ +package app.fedilab.android.client.mastodon.entities; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import com.google.gson.annotations.SerializedName; + + +public class Oembed { + + @SerializedName("type") + public String type; + @SerializedName("version") + public String version; + @SerializedName("title") + public String title; + @SerializedName("author_name") + public String author_name; + @SerializedName("author_url") + public String author_url; + @SerializedName("provider_name") + public String provider_name; + @SerializedName("provider_url") + public String provider_url; + @SerializedName("cache_age") + public long cache_age; + @SerializedName("html") + public String html; + @SerializedName("width") + public int width; + @SerializedName("height") + public int height; +} diff --git a/app/src/main/java/app/fedilab/android/client/mastodon/entities/Pagination.java b/app/src/main/java/app/fedilab/android/client/mastodon/entities/Pagination.java new file mode 100644 index 000000000..30960b149 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/mastodon/entities/Pagination.java @@ -0,0 +1,22 @@ +package app.fedilab.android.client.mastodon.entities; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +public class Pagination { + + public String max_id; + public String min_id; + public String since_id; +} diff --git a/app/src/main/java/app/fedilab/android/client/mastodon/entities/Poll.java b/app/src/main/java/app/fedilab/android/client/mastodon/entities/Poll.java new file mode 100644 index 000000000..a6caf8596 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/mastodon/entities/Poll.java @@ -0,0 +1,59 @@ +package app.fedilab.android.client.mastodon.entities; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import android.text.Spannable; + +import com.google.gson.annotations.SerializedName; + +import java.io.Serializable; +import java.util.Date; +import java.util.List; + +public class Poll implements Serializable { + + @SerializedName("id") + public String id; + @SerializedName("expires_at") + public Date expires_at; + @SerializedName("expire_in") + public int expire_in; + @SerializedName("expired") + public boolean expired; + @SerializedName("multiple") + public boolean multiple; + @SerializedName("votes_count") + public int votes_count; + @SerializedName("voters_count") + public int voters_count; + @SerializedName("voted") + public boolean voted; + @SerializedName("own_votes") + public List own_votes; + @SerializedName("options") + public List options; + @SerializedName("emojis") + public List emojis; + + public static class PollItem implements Serializable { + @SerializedName("title") + public String title; + @SerializedName("votes_count") + public int votes_count; + + //Some extra spannable element - They will be filled automatically when fetching the poll + public transient Spannable span_title; + } +} diff --git a/app/src/main/java/app/fedilab/android/client/mastodon/entities/Preferences.java b/app/src/main/java/app/fedilab/android/client/mastodon/entities/Preferences.java new file mode 100644 index 000000000..f8a2ceb73 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/mastodon/entities/Preferences.java @@ -0,0 +1,31 @@ +package app.fedilab.android.client.mastodon.entities; + +import com.google.gson.annotations.SerializedName; + +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ +public class Preferences { + @SerializedName("posting:default:visibility") + public String visibility; + @SerializedName("posting:default:sensitive") + public boolean sensitive; + @SerializedName("posting:default:language") + public String language; + @SerializedName("reading:default:media") + public String media; + @SerializedName("reading:expand:spoilers") + public boolean spoilers; + +} diff --git a/app/src/main/java/app/fedilab/android/client/mastodon/entities/PushSubscription.java b/app/src/main/java/app/fedilab/android/client/mastodon/entities/PushSubscription.java new file mode 100644 index 000000000..253c2255d --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/mastodon/entities/PushSubscription.java @@ -0,0 +1,42 @@ +package app.fedilab.android.client.mastodon.entities; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import com.google.gson.annotations.SerializedName; + +public class PushSubscription { + + @SerializedName("id") + public String id; + @SerializedName("endpoint") + public String endpoint; + @SerializedName("alerts") + public Alerts alerts; + @SerializedName("server_key") + public String server_key; + + public static class Alerts { + @SerializedName("follow") + public boolean follow; + @SerializedName("favourite") + public boolean favourite; + @SerializedName("reblog") + public boolean reblog; + @SerializedName("mention") + public boolean mention; + @SerializedName("poll") + public boolean poll; + } +} diff --git a/app/src/main/java/app/fedilab/android/client/mastodon/entities/Reaction.java b/app/src/main/java/app/fedilab/android/client/mastodon/entities/Reaction.java new file mode 100644 index 000000000..cee225c19 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/mastodon/entities/Reaction.java @@ -0,0 +1,30 @@ +package app.fedilab.android.client.mastodon.entities; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import com.google.gson.annotations.SerializedName; + +public class Reaction { + @SerializedName("name") + public String name; + @SerializedName("count") + public int count; + @SerializedName("me") + public boolean me; + @SerializedName("url") + public String url; + @SerializedName("static_url") + public String static_url; +} diff --git a/app/src/main/java/app/fedilab/android/client/mastodon/entities/RelationShip.java b/app/src/main/java/app/fedilab/android/client/mastodon/entities/RelationShip.java new file mode 100644 index 000000000..3d4f75ddd --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/mastodon/entities/RelationShip.java @@ -0,0 +1,47 @@ +package app.fedilab.android.client.mastodon.entities; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import com.google.gson.annotations.SerializedName; + +public class RelationShip { + + @SerializedName("id") + public String id; + @SerializedName("following") + public boolean following; + @SerializedName("showing_reblogs") + public boolean showing_reblogs; + @SerializedName("notifying") + public boolean notifying; + @SerializedName("followed_by") + public boolean followed_by; + @SerializedName("blocking") + public boolean blocking; + @SerializedName("blocked_by") + public boolean blocked_by; + @SerializedName("muting") + public boolean muting; + @SerializedName("muting_notifications") + public boolean muting_notifications; + @SerializedName("requested") + public boolean requested; + @SerializedName("domain_blocking") + public boolean domain_blocking; + @SerializedName("endorsed") + public boolean endorsed; + @SerializedName("note") + public String note; +} diff --git a/app/src/main/java/app/fedilab/android/client/mastodon/entities/Report.java b/app/src/main/java/app/fedilab/android/client/mastodon/entities/Report.java new file mode 100644 index 000000000..4b3c1180a --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/mastodon/entities/Report.java @@ -0,0 +1,44 @@ +package app.fedilab.android.client.mastodon.entities; +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import com.google.gson.annotations.SerializedName; + +import java.io.Serializable; +import java.util.List; + +public class Report implements Serializable { + + @SerializedName("id") + public String id; + @SerializedName("action_taken") + public boolean action_taken; + + public static class ReportParams implements Serializable { + @SerializedName("account_id") + public String account_id; + @SerializedName("status_ids") + public List status_ids; + @SerializedName("comment") + public String comment; + @SerializedName("forward") + public Boolean forward; + @SerializedName("category") + public String category; + @SerializedName("rule_ids") + public List rule_ids; + + } +} diff --git a/app/src/main/java/app/fedilab/android/client/mastodon/entities/Results.java b/app/src/main/java/app/fedilab/android/client/mastodon/entities/Results.java new file mode 100644 index 000000000..37a857420 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/mastodon/entities/Results.java @@ -0,0 +1,28 @@ +package app.fedilab.android.client.mastodon.entities; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import com.google.gson.annotations.SerializedName; + +public class Results { + + @SerializedName("accounts") + public java.util.List accounts; + @SerializedName("statuses") + public java.util.List statuses; + @SerializedName("hashtags") + public java.util.List hashtags; + +} diff --git a/app/src/main/java/app/fedilab/android/client/mastodon/entities/ScheduledStatus.java b/app/src/main/java/app/fedilab/android/client/mastodon/entities/ScheduledStatus.java new file mode 100644 index 000000000..66a376ab0 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/mastodon/entities/ScheduledStatus.java @@ -0,0 +1,56 @@ +package app.fedilab.android.client.mastodon.entities; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import com.google.gson.annotations.SerializedName; + +import java.io.Serializable; +import java.util.Date; +import java.util.List; + +public class ScheduledStatus implements Serializable { + @SerializedName("id") + public String id; + @SerializedName("scheduled_at") + public Date scheduled_at; + @SerializedName("params") + public Params params; + @SerializedName("media_attachments") + public List media_attachments; + + + public static class Params implements Serializable { + @SerializedName("text") + public String text; + @SerializedName("media_ids") + public List media_ids; + @SerializedName("sensitive") + public boolean sensitive; + @SerializedName("spoiler_text") + public String spoiler_text; + @SerializedName("visibility") + public String visibility; + @SerializedName("scheduled_at") + public Date scheduled_at; + @SerializedName("poll") + public Poll poll; + @SerializedName("idempotency") + public String idempotency; + @SerializedName("in_reply_to_id") + public String in_reply_to_id; + @SerializedName("application_id") + public String application_id; + } +} diff --git a/app/src/main/java/app/fedilab/android/client/mastodon/entities/ScheduledStatuses.java b/app/src/main/java/app/fedilab/android/client/mastodon/entities/ScheduledStatuses.java new file mode 100644 index 000000000..052393ec5 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/mastodon/entities/ScheduledStatuses.java @@ -0,0 +1,22 @@ +package app.fedilab.android.client.mastodon.entities; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import java.util.List; + +public class ScheduledStatuses { + public Pagination pagination = new Pagination(); + public List scheduledStatuses; +} diff --git a/app/src/main/java/app/fedilab/android/client/mastodon/entities/Source.java b/app/src/main/java/app/fedilab/android/client/mastodon/entities/Source.java new file mode 100644 index 000000000..57469db51 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/mastodon/entities/Source.java @@ -0,0 +1,44 @@ +package app.fedilab.android.client.mastodon.entities; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import com.google.gson.annotations.SerializedName; + +import java.io.Serializable; +import java.util.List; + +public class Source implements Serializable { + @SerializedName("privacy") + public String privacy; + @SerializedName("sensitive") + public boolean sensitive; + @SerializedName("language") + public String language; + @SerializedName("note") + public String note; + @SerializedName("fields") + public List fields; + @SerializedName("follow_requests_count") + public int follow_requests_count; + + public static class SourceParams implements Serializable { + @SerializedName("privacy") + public String privacy; + @SerializedName("sensitive") + public boolean sensitive; + @SerializedName("language") + public String language; + } +} diff --git a/app/src/main/java/app/fedilab/android/client/mastodon/entities/Status.java b/app/src/main/java/app/fedilab/android/client/mastodon/entities/Status.java new file mode 100644 index 000000000..702c6e959 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/mastodon/entities/Status.java @@ -0,0 +1,101 @@ +package app.fedilab.android.client.mastodon.entities; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import android.text.Spannable; + +import com.google.gson.annotations.SerializedName; + +import java.io.Serializable; +import java.util.Date; +import java.util.List; + +public class Status implements Serializable { + + @SerializedName("id") + public String id; + @SerializedName("created_at") + public Date created_at; + @SerializedName("in_reply_to_id") + public String in_reply_to_id; + @SerializedName("in_reply_to_account_id") + public String in_reply_to_account_id; + @SerializedName("sensitive") + public boolean sensitive; + @SerializedName("spoiler_text") + public String spoiler_text; + @SerializedName("text") + public String text; + @SerializedName("visibility") + public String visibility; + @SerializedName("language") + public String language; + @SerializedName("uri") + public String uri; + @SerializedName("url") + public String url; + @SerializedName("replies_count") + public int replies_count; + @SerializedName("reblogs_count") + public int reblogs_count; + @SerializedName("favourites_count") + public int favourites_count; + @SerializedName("favourited") + public boolean favourited; + @SerializedName("reblogged") + public boolean reblogged; + @SerializedName("muted") + public boolean muted; + @SerializedName("bookmarked") + public boolean bookmarked; + @SerializedName("pinned") + public boolean pinned; + @SerializedName("content") + public String content; + @SerializedName("reblog") + public Status reblog; + @SerializedName("application") + public App application; + @SerializedName("account") + public Account account; + @SerializedName("media_attachments") + public List media_attachments; + @SerializedName("mentions") + public List mentions; + @SerializedName("tags") + public List tags; + @SerializedName("emojis") + public List emojis; + @SerializedName("card") + public Card card; + @SerializedName("poll") + public Poll poll; + + //Some extra spannable element - They will be filled automatically when fetching the status + public transient Spannable span_content; + public transient Spannable span_spoiler_text; + public transient Spannable span_translate; + public boolean isExpended = false; + public boolean isTruncated = true; + public boolean isMediaDisplayed = false; + public boolean isMediaObfuscated = true; + public boolean isChecked = false; + public String translationContent; + public boolean translationShown; + public transient boolean isFocused = false; + public transient boolean setCursorToEnd = false; + public transient int cursorPosition = 0; + +} diff --git a/app/src/main/java/app/fedilab/android/client/mastodon/entities/Statuses.java b/app/src/main/java/app/fedilab/android/client/mastodon/entities/Statuses.java new file mode 100644 index 000000000..0e9ebca56 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/mastodon/entities/Statuses.java @@ -0,0 +1,23 @@ +package app.fedilab.android.client.mastodon.entities; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import java.io.Serializable; +import java.util.List; + +public class Statuses implements Serializable { + public Pagination pagination = new Pagination(); + public List statuses; +} diff --git a/app/src/main/java/app/fedilab/android/client/mastodon/entities/Tag.java b/app/src/main/java/app/fedilab/android/client/mastodon/entities/Tag.java new file mode 100644 index 000000000..6affdc795 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/mastodon/entities/Tag.java @@ -0,0 +1,30 @@ +package app.fedilab.android.client.mastodon.entities; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import com.google.gson.annotations.SerializedName; + +import java.io.Serializable; +import java.util.List; + +public class Tag implements Serializable { + + @SerializedName("name") + public String name; + @SerializedName("url") + public String url; + @SerializedName("history") + public List history; +} diff --git a/app/src/main/java/app/fedilab/android/client/mastodon/entities/Token.java b/app/src/main/java/app/fedilab/android/client/mastodon/entities/Token.java new file mode 100644 index 000000000..444d626af --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/mastodon/entities/Token.java @@ -0,0 +1,29 @@ +package app.fedilab.android.client.mastodon.entities; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import com.google.gson.annotations.SerializedName; + +public class Token { + + @SerializedName("access_token") + public String access_token; + @SerializedName("token_type") + public String token_type; + @SerializedName("scope") + public String scope; + @SerializedName("created_at") + public long created_at; +} diff --git a/app/src/main/java/app/fedilab/android/exception/DBException.java b/app/src/main/java/app/fedilab/android/exception/DBException.java new file mode 100644 index 000000000..c49c08997 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/exception/DBException.java @@ -0,0 +1,9 @@ +package app.fedilab.android.exception; + +public class DBException extends Exception { + + // Constructor that accepts a message + public DBException(String message) { + super(message); + } +} diff --git a/app/src/main/java/app/fedilab/android/helper/CacheDataSourceFactory.java b/app/src/main/java/app/fedilab/android/helper/CacheDataSourceFactory.java new file mode 100644 index 000000000..5723f8604 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/helper/CacheDataSourceFactory.java @@ -0,0 +1,78 @@ +package app.fedilab.android.helper; +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import android.content.Context; +import android.content.SharedPreferences; + +import androidx.annotation.NonNull; +import androidx.preference.PreferenceManager; + +import com.google.android.exoplayer2.database.ExoDatabaseProvider; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; +import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; +import com.google.android.exoplayer2.upstream.DefaultHttpDataSource; +import com.google.android.exoplayer2.upstream.FileDataSource; +import com.google.android.exoplayer2.upstream.cache.CacheDataSink; +import com.google.android.exoplayer2.upstream.cache.CacheDataSource; +import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvictor; +import com.google.android.exoplayer2.upstream.cache.SimpleCache; + +import java.io.File; + +import app.fedilab.android.R; + + +public class CacheDataSourceFactory implements DataSource.Factory { + + private static SimpleCache sDownloadCache; + private final Context context; + private final DefaultDataSourceFactory defaultDatasourceFactory; + + public CacheDataSourceFactory(Context context) { + super(); + this.context = context; + DefaultBandwidthMeter.Builder bandwidthMeterBuilder = new DefaultBandwidthMeter.Builder(context); + DefaultBandwidthMeter bandwidthMeter = bandwidthMeterBuilder.build(); + DefaultHttpDataSource.Factory defaultHttpDataSource = new DefaultHttpDataSource.Factory(); + defaultDatasourceFactory = new DefaultDataSourceFactory(this.context, + bandwidthMeter, + defaultHttpDataSource); + } + + public static synchronized SimpleCache getInstance(Context context) { + SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(context); + int video_cache = sharedpreferences.getInt(context.getString(R.string.SET_VIDEO_CACHE), Helper.DEFAULT_VIDEO_CACHE_MB); + + LeastRecentlyUsedCacheEvictor evictor = new LeastRecentlyUsedCacheEvictor(((long) video_cache * 1024 * 1024)); + ExoDatabaseProvider exoDatabaseProvider = new ExoDatabaseProvider(context); + + if (sDownloadCache == null) + sDownloadCache = new SimpleCache(new File(context.getCacheDir(), "media"), evictor, exoDatabaseProvider); + return sDownloadCache; + } + + @NonNull + @Override + public DataSource createDataSource() { + SimpleCache simpleCache = getInstance(context); + SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(context); + int video_cache = sharedpreferences.getInt(context.getString(R.string.SET_VIDEO_CACHE), Helper.DEFAULT_VIDEO_CACHE_MB); + return new CacheDataSource(simpleCache, defaultDatasourceFactory.createDataSource(), + new FileDataSource(), new CacheDataSink(simpleCache, ((long) video_cache * 1024 * 1024)), + CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR, null); + } +} \ No newline at end of file diff --git a/app/src/main/java/app/fedilab/android/helper/CommentDecorationHelper.java b/app/src/main/java/app/fedilab/android/helper/CommentDecorationHelper.java new file mode 100644 index 000000000..0dc2086f7 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/helper/CommentDecorationHelper.java @@ -0,0 +1,71 @@ +package app.fedilab.android.helper; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + + +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +import app.fedilab.android.client.mastodon.entities.Status; + + +public class CommentDecorationHelper { + + /** + * Get the indentation for the reply status with the id of original status + * + * @param replyToCommentId String - The id of the original status if it is a reply + * @param statuses List statuses + * @param maxIndent Maximum number of indents + * @return int - indentation + */ + public static int getIndentation(@NotNull String replyToCommentId, List statuses, int maxIndent) { + return numberOfIndentation(0, replyToCommentId, statuses, maxIndent); + } + + /** + * Returns the indentation depending of the number of replies + * + * @param currentIndentation int - The current indentation (symbolize a margin from start) + * @param replyToCommentId String - The id of the original status if it is a reply + * @param statuses List statuses + * @param maxIndent Maximum number of indents + * @return int - indentation + */ + private static int numberOfIndentation(int currentIndentation, String replyToCommentId, List statuses, int maxIndent) { + if (replyToCommentId == null) { + return 0; + } else { + currentIndentation++; + } + String targetedComment = null; + for (Status status : statuses) { + if (replyToCommentId.compareTo(status.id) == 0) { + targetedComment = status.in_reply_to_id; + break; + } + } + if (targetedComment != null) { + return numberOfIndentation(currentIndentation, targetedComment, statuses, maxIndent); + } else { + if (currentIndentation == 0) { + currentIndentation = 1; + } + return Math.min(currentIndentation, maxIndent); + } + } + +} diff --git a/app/src/main/java/app/fedilab/android/helper/CrossActionHelper.java b/app/src/main/java/app/fedilab/android/helper/CrossActionHelper.java new file mode 100644 index 000000000..2c4057da3 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/helper/CrossActionHelper.java @@ -0,0 +1,208 @@ +package app.fedilab.android.helper; +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import android.content.Context; +import android.os.Handler; +import android.os.Looper; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.ViewModelProvider; +import androidx.lifecycle.ViewModelStoreOwner; + +import java.util.ArrayList; +import java.util.List; + +import app.fedilab.android.BaseMainActivity; +import app.fedilab.android.R; +import app.fedilab.android.client.entities.Account; +import app.fedilab.android.client.mastodon.entities.Status; +import app.fedilab.android.exception.DBException; +import app.fedilab.android.ui.drawer.AccountsSearchAdapter; +import app.fedilab.android.viewmodel.mastodon.AccountsVM; +import app.fedilab.android.viewmodel.mastodon.SearchVM; +import app.fedilab.android.viewmodel.mastodon.StatusesVM; +import es.dmoral.toasty.Toasty; + +public class CrossActionHelper { + + + /** + * Allow to do the action with another account from db + * + * @param context Context + * @param actionType enum TypeOfCrossAction + * @param targetedAccount mastodon account that is targeted + * @param targetedStatus status that is targeted + */ + public static void doCrossAction(@NonNull Context context, @NonNull TypeOfCrossAction actionType, app.fedilab.android.client.mastodon.entities.Account targetedAccount, Status targetedStatus) { + + new Thread(() -> { + try { + List accounts = new Account(context).getCrossAccounts(); + if (accounts.size() == 1) { + Handler mainHandler = new Handler(Looper.getMainLooper()); + Runnable myRunnable = () -> fetchRemote(context, actionType, accounts.get(0), targetedAccount, targetedStatus); + mainHandler.post(myRunnable); + } else { + List accountList = new ArrayList<>(); + for (Account account : accounts) { + accountList.add(account.mastodon_account); + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + Runnable myRunnable = () -> { + AlertDialog.Builder builderSingle = new AlertDialog.Builder(context, Helper.dialogStyle()); + builderSingle.setTitle(context.getString(R.string.choose_accounts)); + final AccountsSearchAdapter accountsSearchAdapter = new AccountsSearchAdapter(context, accountList); + final Account[] accountArray = new Account[accounts.size()]; + int i = 0; + for (Account account : accounts) { + accountArray[i] = account; + i++; + } + builderSingle.setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss()); + builderSingle.setAdapter(accountsSearchAdapter, (dialog, which) -> { + Account selectedAccount = accountArray[which]; + fetchRemote(context, actionType, selectedAccount, targetedAccount, targetedStatus); + dialog.dismiss(); + }); + builderSingle.show(); + }; + mainHandler.post(myRunnable); + } + + } catch (DBException e) { + e.printStackTrace(); + } + }).start(); + + } + + /** + * Fetch and federate the remote account or status + */ + private static void fetchRemote(@NonNull Context context, @NonNull TypeOfCrossAction actionType, @NonNull Account ownerAccount, app.fedilab.android.client.mastodon.entities.Account targetedAccount, Status targetedStatus) { + + SearchVM searchVM = new ViewModelProvider((ViewModelStoreOwner) context).get("crossactions", SearchVM.class); + if (targetedAccount != null) { + String search; + if (targetedAccount.acct.contains("@")) { //Not from same instance + search = targetedAccount.acct; + } else { + search = targetedAccount.acct + "@" + BaseMainActivity.currentInstance; + } + searchVM.search(ownerAccount.instance, ownerAccount.token, search, null, "accounts", false, true, false, 0, null, null, 1) + .observe((LifecycleOwner) context, results -> { + if (results.accounts != null && results.accounts.size() > 0) { + app.fedilab.android.client.mastodon.entities.Account account = results.accounts.get(0); + applyAction(context, actionType, ownerAccount, account, null); + } else { + Toasty.info(context, context.getString(R.string.toast_error_search), Toasty.LENGTH_SHORT).show(); + } + }); + } else if (targetedStatus != null) { + searchVM.search(ownerAccount.instance, ownerAccount.token, targetedStatus.url, null, "statuses", false, true, false, 0, null, null, 1) + .observe((LifecycleOwner) context, results -> { + if (results.statuses != null && results.statuses.size() > 0) { + Status status = results.statuses.get(0); + applyAction(context, actionType, ownerAccount, null, status); + } else { + Toasty.info(context, context.getString(R.string.toast_error_search), Toasty.LENGTH_SHORT).show(); + } + }); + } else { + Toasty.info(context, context.getString(R.string.toast_error_search), Toasty.LENGTH_SHORT).show(); + } + + } + + /** + * Do action when status or account has been fetched + */ + private static void applyAction(@NonNull Context context, @NonNull TypeOfCrossAction actionType, @NonNull Account ownerAccount, app.fedilab.android.client.mastodon.entities.Account targetedAccount, Status targetedStatus) { + + AccountsVM accountsVM = null; + StatusesVM statusesVM = null; + if (targetedAccount != null) { + accountsVM = new ViewModelProvider((ViewModelStoreOwner) context).get("crossactions", AccountsVM.class); + } + if (targetedStatus != null) { + statusesVM = new ViewModelProvider((ViewModelStoreOwner) context).get("crossactions", StatusesVM.class); + } + switch (actionType) { + case MUTE_ACTION: + assert accountsVM != null; + accountsVM.mute(ownerAccount.instance, ownerAccount.token, targetedAccount.id, true, 0) + .observe((LifecycleOwner) context, relationShip -> Toasty.info(context, context.getString(R.string.toast_mute), Toasty.LENGTH_SHORT).show()); + break; + case UNMUTE_ACTION: + assert accountsVM != null; + accountsVM.unmute(ownerAccount.instance, ownerAccount.token, targetedAccount.id) + .observe((LifecycleOwner) context, relationShip -> Toasty.info(context, context.getString(R.string.toast_unmute), Toasty.LENGTH_SHORT).show()); + break; + case BLOCK_ACTION: + assert accountsVM != null; + accountsVM.block(ownerAccount.instance, ownerAccount.token, targetedAccount.id) + .observe((LifecycleOwner) context, relationShip -> Toasty.info(context, context.getString(R.string.toast_block), Toasty.LENGTH_SHORT).show()); + break; + case UNBLOCK_ACTION: + assert accountsVM != null; + accountsVM.unblock(ownerAccount.instance, ownerAccount.token, targetedAccount.id) + .observe((LifecycleOwner) context, relationShip -> Toasty.info(context, context.getString(R.string.toast_unblock), Toasty.LENGTH_SHORT).show()); + break; + case FOLLOW_ACTION: + assert accountsVM != null; + accountsVM.follow(ownerAccount.instance, ownerAccount.token, targetedAccount.id, true, false) + .observe((LifecycleOwner) context, relationShip -> Toasty.info(context, context.getString(R.string.toast_follow), Toasty.LENGTH_SHORT).show()); + break; + case UNFOLLOW_ACTION: + assert accountsVM != null; + accountsVM.unfollow(ownerAccount.instance, ownerAccount.token, targetedAccount.id) + .observe((LifecycleOwner) context, relationShip -> Toasty.info(context, context.getString(R.string.toast_unfollow), Toasty.LENGTH_SHORT).show()); + break; + case FAVOURITE_ACTION: + assert statusesVM != null; + statusesVM.favourite(ownerAccount.instance, ownerAccount.token, targetedStatus.id) + .observe((LifecycleOwner) context, status -> Toasty.info(context, context.getString(R.string.toast_favourite), Toasty.LENGTH_SHORT).show()); + case UNFAVOURITE_ACTION: + assert statusesVM != null; + statusesVM.unFavourite(ownerAccount.instance, ownerAccount.token, targetedStatus.id) + .observe((LifecycleOwner) context, status -> Toasty.info(context, context.getString(R.string.toast_unfavourite), Toasty.LENGTH_SHORT).show()); + case REBLOG_ACTION: + assert statusesVM != null; + statusesVM.reblog(ownerAccount.instance, ownerAccount.token, targetedStatus.id, null) + .observe((LifecycleOwner) context, status -> Toasty.info(context, context.getString(R.string.toast_reblog), Toasty.LENGTH_SHORT).show()); + case UNREBLOG_ACTION: + assert statusesVM != null; + statusesVM.unReblog(ownerAccount.instance, ownerAccount.token, targetedStatus.id) + .observe((LifecycleOwner) context, status -> Toasty.info(context, context.getString(R.string.toast_unreblog), Toasty.LENGTH_SHORT).show()); + } + } + + public enum TypeOfCrossAction { + FOLLOW_ACTION, + UNFOLLOW_ACTION, + MUTE_ACTION, + UNMUTE_ACTION, + BLOCK_ACTION, + UNBLOCK_ACTION, + FAVOURITE_ACTION, + UNFAVOURITE_ACTION, + REBLOG_ACTION, + UNREBLOG_ACTION + } +} diff --git a/app/src/main/java/app/fedilab/android/helper/DividerDecoration.java b/app/src/main/java/app/fedilab/android/helper/DividerDecoration.java new file mode 100644 index 000000000..f8b2dcaf8 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/helper/DividerDecoration.java @@ -0,0 +1,145 @@ +package app.fedilab.android.helper; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.DashPathEffect; +import android.graphics.Paint; +import android.graphics.Rect; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.core.content.res.ResourcesCompat; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.Arrays; +import java.util.List; + +import app.fedilab.android.R; +import app.fedilab.android.client.mastodon.entities.Status; +import app.fedilab.android.ui.drawer.StatusAdapter; + +public class DividerDecoration extends RecyclerView.ItemDecoration { + + private final Context _mContext; + private final List statusList; + private final List colorList = Arrays.asList( + R.color.decoration_1, + R.color.decoration_2, + R.color.decoration_3, + R.color.decoration_4, + R.color.decoration_5, + R.color.decoration_6, + R.color.decoration_7, + R.color.decoration_8, + R.color.decoration_9, + R.color.decoration_10, + R.color.decoration_11, + R.color.decoration_12, + R.color.decoration_13, + R.color.decoration_14, + R.color.decoration_15 + ); + + public DividerDecoration(Context context, List statuses) { + _mContext = context; + statusList = statuses; + } + + @Override + public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { + int position = ((RecyclerView.LayoutParams) view.getLayoutParams()).getViewAdapterPosition(); + StatusAdapter statusAdapter = ((StatusAdapter) parent.getAdapter()); + if (statusAdapter != null && statusAdapter.getItemCount() > position && position > 0) { + Status status = statusAdapter.getItem(position); + + int start = (int) Helper.convertDpToPixel( + 6 * CommentDecorationHelper.getIndentation(status.in_reply_to_id, statusList, colorList.size()), + _mContext); + + if (parent.getLayoutDirection() == View.LAYOUT_DIRECTION_LTR) { + outRect.set(start, 0, 0, 0); + } else { + outRect.set(0, 0, start, 0); + } + } + } + + @Override + public void onDraw(@NonNull Canvas c, RecyclerView parent, @NonNull RecyclerView.State state) { + int margin = (int) Helper.convertDpToPixel(12, _mContext); + for (int i = 0; i < parent.getChildCount(); i++) { + View view = parent.getChildAt(i); + int position = parent.getChildAdapterPosition(view); + StatusAdapter statusAdapter = ((StatusAdapter) parent.getAdapter()); + if (statusAdapter != null && position >= 0) { + Status status = statusAdapter.getItem(position); + + int indentation = Math.min( + CommentDecorationHelper.getIndentation(status.in_reply_to_id, statusList, colorList.size()), + colorList.size()); + + if (indentation > 0) { + Paint paint = new Paint(); + paint.setDither(false); + paint.setStrokeWidth(Helper.convertDpToPixel(1.5F, _mContext)); + paint.setStrokeCap(Paint.Cap.BUTT); + paint.setStrokeJoin(Paint.Join.MITER); + + for (int j = 0; j < indentation; j++) { + float startPx = Helper.convertDpToPixel(6 + 6 * j, _mContext); + if (parent.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) + startPx = c.getWidth() - startPx; + + float bottomPx = view.getBottom(); + + paint.setColor(ResourcesCompat.getColor(_mContext.getResources(), colorList.get(j), _mContext.getTheme())); + if (j == colorList.size() - 1) { + paint.setPathEffect(new DashPathEffect( + new float[]{Helper.convertDpToPixel(3, _mContext), Helper.convertDpToPixel(3, _mContext)}, + 0)); + bottomPx = bottomPx - view.getHeight() / 2F; + } + + c.drawLine(startPx, view.getTop() - margin, startPx, bottomPx, paint); + } + + paint.setColor(ResourcesCompat.getColor(_mContext.getResources(), colorList.get(indentation - 1), _mContext.getTheme())); + + float startDp = 6 * (indentation - 1) + 6; + float centerPx = view.getBottom() - view.getHeight() / 2F; + float endDp = startDp + 12; + float endPx = Helper.convertDpToPixel(endDp, _mContext); + + float startPx = Helper.convertDpToPixel(startDp, _mContext); + if (parent.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) { + startPx = c.getWidth() - startPx; + endPx = c.getWidth() - endPx; + } + + if (i > 0) { + View aboveView = parent.getChildAt(i - 1); + float aboveViewLineTopPx = (aboveView.getTop() + aboveView.getHeight() / 2F) + Helper.convertDpToPixel(0.75F, _mContext); + float aboveViewLineBottomPx = parent.getChildAt(i - 1).getBottom(); + c.drawLine(startPx, aboveViewLineTopPx, startPx, aboveViewLineBottomPx, paint); + } + + c.drawLine(startPx, centerPx, endPx, centerPx, paint); + } + } + } + } +} diff --git a/app/src/main/java/app/fedilab/android/helper/DividerDecorationSimple.java b/app/src/main/java/app/fedilab/android/helper/DividerDecorationSimple.java new file mode 100644 index 000000000..f67bb5420 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/helper/DividerDecorationSimple.java @@ -0,0 +1,123 @@ +package app.fedilab.android.helper; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.DashPathEffect; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.Rect; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.core.content.res.ResourcesCompat; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.List; + +import app.fedilab.android.R; +import app.fedilab.android.client.mastodon.entities.Status; +import app.fedilab.android.ui.drawer.ComposeAdapter; + +public class DividerDecorationSimple extends RecyclerView.ItemDecoration { + + private final Context _mContext; + private final List statusList; + + public DividerDecorationSimple(Context context, List statuses) { + _mContext = context; + statusList = statuses; + } + + @Override + public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { + int position = ((RecyclerView.LayoutParams) view.getLayoutParams()).getViewAdapterPosition(); + ComposeAdapter composeAdapter = ((ComposeAdapter) parent.getAdapter()); + if (composeAdapter != null && composeAdapter.getItemCount() > position && position >= 0) { + Status status = composeAdapter.getItem(position); + if (status != null) { + int start = (int) Helper.convertDpToPixel( + 4 * CommentDecorationHelper.getIndentation(status.in_reply_to_id, statusList, 15), + _mContext); + + if (parent.getLayoutDirection() == View.LAYOUT_DIRECTION_LTR) { + outRect.set(start, 0, 0, 0); + } else { + outRect.set(0, 0, start, 0); + } + } + } + } + + @Override + public void onDraw(@NonNull Canvas c, RecyclerView parent, @NonNull RecyclerView.State state) { + for (int i = 0; i < parent.getChildCount(); i++) { + View view = parent.getChildAt(i); + int position = parent.getChildAdapterPosition(view); + ComposeAdapter composeAdapter = ((ComposeAdapter) parent.getAdapter()); + if (composeAdapter != null && composeAdapter.getItemCount() > position && position >= 0) { + Status status = composeAdapter.getItem(position); + + if (status != null) { + int indentation = CommentDecorationHelper.getIndentation(status.in_reply_to_id, statusList, 15); + + if (indentation > 0) { + Paint paint = new Paint(); + paint.setDither(false); + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(Helper.convertDpToPixel(2F, _mContext)); + paint.setStrokeCap(Paint.Cap.ROUND); + paint.setStrokeJoin(Paint.Join.ROUND); + paint.setColor(ResourcesCompat.getColor(_mContext.getResources(), R.color.cyanea_accent, _mContext.getTheme())); + if (indentation == 15) { + paint.setPathEffect(new DashPathEffect( + new float[]{Helper.convertDpToPixel(3, _mContext), Helper.convertDpToPixel(3, _mContext)}, + 0)); + } + + float startDp = 12 + 4 * (indentation - 1); + if (i > 0) startDp = startDp - 6; + + float endDp = startDp + 4; + if (i > 0) endDp = endDp + 4; + + float startPx = Helper.convertDpToPixel(startDp, _mContext); + if (parent.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) { + startPx = c.getWidth() - startPx; + } + + float topPx = view.getTop() - Helper.convertDpToPixel(12, _mContext); + + if (i > 0) { + View aboveView = parent.getChildAt(i - 1); + topPx = topPx - (aboveView.getHeight() / 2F); + } + + float bottomPx = view.getBottom() - view.getHeight() / 2F; + float endPx = Helper.convertDpToPixel(endDp, _mContext); + + Path path = new Path(); + path.moveTo(startPx, topPx); + path.lineTo(startPx, bottomPx); + path.lineTo(endPx, bottomPx); + + c.drawPath(path, paint); + } + } + } + } + } +} diff --git a/app/src/main/java/app/fedilab/android/helper/ECDH.java b/app/src/main/java/app/fedilab/android/helper/ECDH.java new file mode 100644 index 000000000..e3d79e7d2 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/helper/ECDH.java @@ -0,0 +1,289 @@ +package app.fedilab.android.helper; +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.SharedPreferences; +import android.util.Base64; + +import androidx.preference.PreferenceManager; + +import org.spongycastle.asn1.ASN1ObjectIdentifier; +import org.spongycastle.asn1.x9.ECNamedCurveTable; +import org.spongycastle.asn1.x9.X9ECParameters; +import org.spongycastle.crypto.params.ECNamedDomainParameters; +import org.spongycastle.crypto.params.ECPrivateKeyParameters; +import org.spongycastle.crypto.params.ECPublicKeyParameters; +import org.spongycastle.jce.spec.ECNamedCurveSpec; +import org.spongycastle.jce.spec.ECParameterSpec; +import org.spongycastle.jce.spec.ECPrivateKeySpec; +import org.spongycastle.jce.spec.ECPublicKeySpec; +import org.spongycastle.math.ec.ECCurve; +import org.spongycastle.math.ec.ECPoint; + +import java.math.BigInteger; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.Security; +import java.security.interfaces.ECPublicKey; +import java.security.spec.ECGenParameterSpec; + +import javax.crypto.Cipher; +import javax.crypto.KeyAgreement; + + +public class ECDH { + + + public static final String kp_public = "kp_public"; + public static final String peer_public = "peer_public"; + public static final String PROVIDER = org.spongycastle.jce.provider.BouncyCastleProvider.PROVIDER_NAME; + + public static final String kp_private = "kp_private"; + public static final String KEGEN_ALG = "ECDH"; + + public static final String name = "prime256v1"; + + private static final String kp_public_affine_x = "kp_public_affine_x"; + private static final String kp_public_affine_y = "kp_public_affine_y"; + + private static ECDH instance; + + static { + Security.addProvider(new org.spongycastle.jce.provider.BouncyCastleProvider()); + } + + public final KeyFactory kf; + private final KeyPairGenerator kpg; + private final String slug; + + public ECDH(String slug) throws Exception { + if (slug == null) { + throw new Exception("slug cannot be null"); + } + try { + kf = KeyFactory.getInstance(KEGEN_ALG, PROVIDER); + kpg = KeyPairGenerator.getInstance(KEGEN_ALG, PROVIDER); + this.slug = slug; + } catch (NoSuchAlgorithmException | NoSuchProviderException e) { + e.printStackTrace(); + throw new RuntimeException(e); + } + } + + public static synchronized ECDH getInstance(String slug) throws Exception { + if (instance == null) { + instance = new ECDH(slug); + } + return instance; + } + + public static String base64Encode(byte[] b) { + return Base64.encodeToString( + b, Base64.URL_SAFE | Base64.NO_PADDING | Base64.NO_WRAP); + } + + static byte[] base64Decode(String str) { + return Base64.decode(str, Base64.URL_SAFE); + } + + synchronized KeyPair generateKeyPair() + throws Exception { + ECGenParameterSpec ecParamSpec = new ECGenParameterSpec(name); + kpg.initialize(ecParamSpec); + + return kpg.generateKeyPair(); + } + + private byte[] generateSecret(PrivateKey myPrivKey, PublicKey otherPubKey) throws Exception { + KeyAgreement keyAgreement = KeyAgreement.getInstance(KEGEN_ALG); + keyAgreement.init(myPrivKey); + keyAgreement.doPhase(otherPubKey, true); + + return keyAgreement.generateSecret(); + } + + + synchronized KeyPair readKeyPair(Context context) + throws Exception { + return new KeyPair(readMyPublicKey(context), readMyPrivateKey(context)); + } + + @SuppressLint("ApplySharedPref") + public KeyPair newPair(Context context) { + SharedPreferences.Editor prefsEditor = PreferenceManager + .getDefaultSharedPreferences(context).edit(); + KeyPair kp; + try { + kp = generateKeyPair(); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + + ECPublicKey key = (ECPublicKey) kp.getPublic(); + byte[] x = key.getW().getAffineX().toByteArray(); + byte[] y = key.getW().getAffineY().toByteArray(); + BigInteger xbi = new BigInteger(1, x); + BigInteger ybi = new BigInteger(1, y); + X9ECParameters x9 = ECNamedCurveTable.getByName(name); + ASN1ObjectIdentifier oid = ECNamedCurveTable.getOID(name); + + ECCurve curve = x9.getCurve(); + ECPoint point = curve.createPoint(xbi, ybi); + ECNamedDomainParameters dParams = new ECNamedDomainParameters(oid, + x9.getCurve(), x9.getG(), x9.getN(), x9.getH(), x9.getSeed()); + + ECPublicKeyParameters pubKey = new ECPublicKeyParameters(point, dParams); + + + ECPrivateKeyParameters privateKey = new ECPrivateKeyParameters(new BigInteger(kp.getPrivate().getEncoded()), pubKey.getParameters()); + byte[] privateKeyBytes = privateKey.getD().toByteArray(); + + String keyString = base64Encode(pubKey.getQ().getEncoded(false)); + String keypString = base64Encode(privateKeyBytes); + prefsEditor.putString(kp_public + slug, keyString); + prefsEditor.putString(kp_public_affine_x + slug, key.getW().getAffineX().toString()); + prefsEditor.putString(kp_public_affine_y + slug, key.getW().getAffineY().toString()); + prefsEditor.putString(kp_private + slug, keypString); + prefsEditor.commit(); + return kp; + } + + + synchronized PublicKey readMyPublicKey(Context context) throws Exception { + + X9ECParameters x9 = ECNamedCurveTable.getByName(name); + ASN1ObjectIdentifier oid = ECNamedCurveTable.getOID(name); + + SharedPreferences prefs = PreferenceManager + .getDefaultSharedPreferences(context); + BigInteger xbi = new BigInteger(prefs.getString(kp_public_affine_x + slug, "0")); + BigInteger ybi = new BigInteger(prefs.getString(kp_public_affine_y + slug, "0")); + + ECNamedDomainParameters dParams = new ECNamedDomainParameters(oid, + x9.getCurve(), x9.getG(), x9.getN(), x9.getH(), x9.getSeed()); + + + ECNamedCurveSpec ecNamedCurveSpec = new ECNamedCurveSpec(name, dParams.getCurve(), dParams.getG(), dParams.getN()); + java.security.spec.ECPoint w = new java.security.spec.ECPoint(xbi, ybi); + return kf.generatePublic(new java.security.spec.ECPublicKeySpec(w, ecNamedCurveSpec)); + } + + + public String uncryptMessage(Context context, String cyphered) { + byte[] privateKey = getSharedSecret(context); + try { + Cipher outCipher = Cipher.getInstance("ECIES", PROVIDER); + PrivateKey ddd = readPrivateKey(privateKey); + outCipher.init(Cipher.DECRYPT_MODE, readPrivateKey(privateKey)); + byte[] plaintext = outCipher.doFinal(base64Decode(cyphered)); + return new String(plaintext); + } catch (Exception e) { + e.printStackTrace(); + } + return ""; + + } + + + public PublicKey readPublicKey(String keyStr) throws Exception { + ECParameterSpec parameterSpec = org.spongycastle.jce.ECNamedCurveTable.getParameterSpec(name); + ECCurve curve = parameterSpec.getCurve(); + ECPoint point = curve.decodePoint(base64Decode(keyStr)); + ECPublicKeySpec pubSpec = new ECPublicKeySpec(point, parameterSpec); + return kf.generatePublic(pubSpec); + } + + + public PrivateKey readPrivateKey(byte[] key) throws Exception { + ECParameterSpec parameterSpec = org.spongycastle.jce.ECNamedCurveTable.getParameterSpec(name); + ECPrivateKeySpec pubSpec = new ECPrivateKeySpec(new BigInteger(1, key), parameterSpec); + return kf.generatePrivate(pubSpec); + } + + synchronized PrivateKey readMyPrivateKey(Context context) throws Exception { + X9ECParameters x9 = ECNamedCurveTable.getByName(name); + ASN1ObjectIdentifier oid = ECNamedCurveTable.getOID(name); + + SharedPreferences prefs = PreferenceManager + .getDefaultSharedPreferences(context); + BigInteger ybi = new BigInteger(prefs.getString(kp_public_affine_y + slug, "0")); + ECNamedDomainParameters dParams = new ECNamedDomainParameters(oid, + x9.getCurve(), x9.getG(), x9.getN(), x9.getH(), x9.getSeed()); + ECNamedCurveSpec ecNamedCurveSpec = new ECNamedCurveSpec(name, dParams.getCurve(), dParams.getG(), dParams.getN()); + return kf.generatePrivate(new java.security.spec.ECPrivateKeySpec(ybi, ecNamedCurveSpec)); + } + + + private synchronized KeyPair getPair(Context context) { + SharedPreferences prefs = PreferenceManager + .getDefaultSharedPreferences(context); + String strPub = prefs.getString(kp_public + slug, ""); + String strPriv = prefs.getString(kp_private + slug, ""); + if (strPub.trim().isEmpty() || strPriv.trim().isEmpty()) { + return newPair(context); + } + try { + return readKeyPair(context); + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + PublicKey getServerKey(Context context) throws Exception { + SharedPreferences prefs = PreferenceManager + .getDefaultSharedPreferences(context); + String serverKey = prefs.getString(peer_public + slug, ""); + return readPublicKey(serverKey); + } + + @SuppressWarnings({"unused", "RedundantSuppression"}) + public byte[] getSharedSecret(Context context) { + try { + KeyPair keyPair = getPair(context); + if (keyPair != null) { + return generateSecret(keyPair.getPrivate(), getServerKey(context)); + } + } catch (Exception e) { + e.printStackTrace(); + return null; + } + return null; + } + + public String getPublicKey(Context context) { + SharedPreferences prefs = PreferenceManager + .getDefaultSharedPreferences(context); + + return prefs.getString(kp_public + slug, ""); + } + + @SuppressLint("ApplySharedPref") + public void saveServerKey(Context context, String strPeerPublic) { + SharedPreferences.Editor prefsEditor = PreferenceManager + .getDefaultSharedPreferences(context).edit(); + + prefsEditor.putString(peer_public + slug, strPeerPublic); + prefsEditor.commit(); + } +} diff --git a/app/src/main/java/app/fedilab/android/helper/Helper.java b/app/src/main/java/app/fedilab/android/helper/Helper.java new file mode 100644 index 000000000..0be4cd019 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/helper/Helper.java @@ -0,0 +1,1391 @@ +package app.fedilab.android.helper; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import static android.content.Context.DOWNLOAD_SERVICE; +import static app.fedilab.android.webview.ProxyHelper.setProxy; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.app.DownloadManager; +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; +import android.graphics.drawable.Drawable; +import android.media.AudioAttributes; +import android.media.RingtoneManager; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.Environment; +import android.os.Handler; +import android.os.Looper; +import android.provider.MediaStore; +import android.provider.OpenableColumns; +import android.text.TextUtils; +import android.util.DisplayMetrics; +import android.util.TypedValue; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.WindowManager; +import android.webkit.CookieManager; +import android.webkit.MimeTypeMap; +import android.webkit.URLUtil; +import android.webkit.WebChromeClient; +import android.webkit.WebSettings; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.IdRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.widget.SwitchCompat; +import androidx.core.app.NotificationCompat; +import androidx.core.app.NotificationManagerCompat; +import androidx.core.content.ContextCompat; +import androidx.core.graphics.drawable.DrawableCompat; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentTransaction; +import androidx.lifecycle.ViewModelProvider; +import androidx.lifecycle.ViewModelStoreOwner; +import androidx.preference.PreferenceManager; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.load.resource.bitmap.CenterCrop; +import com.bumptech.glide.load.resource.bitmap.RoundedCorners; +import com.bumptech.glide.request.RequestOptions; +import com.jaredrummler.cyanea.Cyanea; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.ref.WeakReference; +import java.net.Authenticator; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.PasswordAuthentication; +import java.net.Proxy; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Random; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import app.fedilab.android.BaseMainActivity; +import app.fedilab.android.R; +import app.fedilab.android.activities.LoginActivity; +import app.fedilab.android.activities.WebviewActivity; +import app.fedilab.android.broadcastreceiver.ToastMessage; +import app.fedilab.android.client.entities.Account; +import app.fedilab.android.client.mastodon.entities.Attachment; +import app.fedilab.android.exception.DBException; +import app.fedilab.android.sqlite.Sqlite; +import app.fedilab.android.viewmodel.mastodon.OauthVM; +import app.fedilab.android.webview.CustomWebview; +import es.dmoral.toasty.Toasty; +import okhttp3.MediaType; +import okhttp3.MultipartBody; +import okhttp3.RequestBody; + +public class Helper { + + public static final String TAG = "fedilab_app"; + public static final String APP_CLIENT_ID = "APP_CLIENT_ID"; + public static final String APP_CLIENT_SECRET = "APP_CLIENT_SECRET"; + public static final String APP_INSTANCE = "APP_INSTANCE"; + public static final String APP_API = "APP_API"; + public static final String CLIP_BOARD = "CLIP_BOARD"; + + public static final String INSTANCE_SOCIAL_KEY = "jGj9gW3z9ptyIpB8CMGhAlTlslcemMV6AgoiImfw3vPP98birAJTHOWiu5ZWfCkLvcaLsFZw9e3Pb7TIwkbIyrj3z6S7r2oE6uy6EFHvls3YtapP8QKNZ980p9RfzTb4"; + public static final String WEBSITE_VALUE = "https://fedilab.app"; + + public static final String RECEIVE_TOAST_MESSAGE = "RECEIVE_TOAST_MESSAGE"; + public static final String RECEIVE_TOAST_TYPE = "RECEIVE_TOAST_TYPE"; + public static final String RECEIVE_TOAST_CONTENT = "RECEIVE_TOAST_CONTENT"; + public static final String RECEIVE_TOAST_TYPE_ERROR = "RECEIVE_TOAST_TYPE_ERROR"; + public static final String RECEIVE_TOAST_TYPE_INFO = "RECEIVE_TOAST_TYPE_INFO"; + public static final String RECEIVE_TOAST_TYPE_SUCCESS = "RECEIVE_TOAST_TYPE_SUCCESS"; + public static final String RECEIVE_TOAST_TYPE_WARNING = "RECEIVE_TOAST_TYPE_WARNING"; + + //Intent + public static final String INTENT_ACTION = "intent_action"; + + public static final String BROADCAST_DATA = "BROADCAST_DATA"; + public static final String RECEIVE_REDRAW_TOPBAR = "RECEIVE_REDRAW_TOPBAR"; + public static final String RECEIVE_RECREATE_ACTIVITY = "RECEIVE_RECREATE_ACTIVITY"; + public static final String RECEIVE_MASTODON_LIST = "RECEIVE_MASTODON_LIST"; + public static final String RECEIVE_REDRAW_PROFILE = "RECEIVE_REDRAW_PROFILE"; + + public static final String ARG_TIMELINE_TYPE = "ARG_TIMELINE_TYPE"; + public static final String ARG_NOTIFICATION_TYPE = "ARG_NOTIFICATION_TYPE"; + public static final String ARG_EXCLUDED_NOTIFICATION_TYPE = "ARG_EXCLUDED_NOTIFICATION_TYPE"; + public static final String ARG_STATUS = "ARG_STATUS"; + public static final String ARG_STATUS_DRAFT = "ARG_STATUS_DRAFT"; + public static final String ARG_STATUS_SCHEDULED = "ARG_STATUS_SCHEDULED"; + + public static final String ARG_STATUS_DRAFT_ID = "ARG_STATUS_DRAFT_ID"; + public static final String ARG_STATUS_REPLY = "ARG_STATUS_REPLY"; + public static final String ARG_ACCOUNT = "ARG_ACCOUNT"; + public static final String ARG_MINIFIED = "ARG_MINIFIED"; + public static final String ARG_STATUS_REPORT = "ARG_STATUS_REPORT"; + public static final String ARG_STATUS_MENTION = "ARG_STATUS_MENTION"; + public static final String ARG_FOLLOW_TYPE = "ARG_FOLLOW_TYPE"; + public static final String ARG_TYPE_OF_INFO = "ARG_TYPE_OF_INFO"; + public static final String ARG_TOKEN = "ARG_TOKEN"; + public static final String ARG_INSTANCE = "ARG_INSTANCE"; + public static final String ARG_STATUS_ID = "ARG_STATUS_ID"; + public static final String ARG_WORK_ID = "ARG_WORK_ID"; + public static final String ARG_LIST_ID = "ARG_LIST_ID"; + public static final String ARG_SEARCH_KEYWORD = "ARG_SEARCH_KEYWORD"; + public static final String ARG_SEARCH_TYPE = "ARG_SEARCH_TYPE"; + public static final String ARG_SEARCH_KEYWORD_CACHE = "ARG_SEARCH_KEYWORD_CACHE"; + public static final String ARG_VIEW_MODEL_KEY = "ARG_VIEW_MODEL_KEY"; + public static final String ARG_TAG_TIMELINE = "ARG_TAG_TIMELINE"; + public static final String ARG_MEDIA_POSITION = "ARG_MEDIA_POSITION"; + public static final String ARG_MEDIA_ATTACHMENT = "ARG_MEDIA_ATTACHMENT"; + public static final String ARG_SHOW_REPLIES = "ARG_SHOW_REPLIES"; + public static final String ARG_SHOW_PINNED = "ARG_SHOW_PINNED"; + public static final String ARG_SHOW_MEDIA_ONY = "ARG_SHOW_MEDIA_ONY"; + public static final String ARG_MENTION = "ARG_MENTION"; + public static final String ARG_USER_ID = "ARG_USER_ID"; + public static final String ARG_MEDIA_ARRAY = "ARG_MEDIA_ARRAY"; + public static final String ARG_VISIBILITY = "ARG_VISIBILITY"; + public static final String ARG_SCHEDULED_DATE = "ARG_SCHEDULED_DATE"; + + public static final String WORKER_REFRESH_NOTIFICATION = "WORKER_REFRESH_NOTIFICATION"; + public static final String WORKER_SCHEDULED_STATUSES = "WORKER_SCHEDULED_STATUSES"; + public static final String WORKER_SCHEDULED_REBLOGS = "WORKER_SCHEDULED_REBLOGS"; + + public static final String VALUE_TRENDS = "VALUE_TRENDS"; + + public static final String USER_AGENT = "Mozilla/5.0 (Windows NT 6.1; rv:60.0) Gecko/20100101 Firefox/60.0"; + public static final String REDIRECT_CONTENT_WEB = "fedilab://backtofedilab"; + public static final String REDIRECT_CONTENT = "urn:ietf:wg:oauth:2.0:oob"; + public static final String APP_OAUTH_SCOPES = "read write"; + public static final String OAUTH_SCOPES = "read write follow push"; + public static final String OAUTH_SCOPES_ADMIN = "read write follow push admin:read admin:write"; + public static final int DEFAULT_VIDEO_CACHE_MB = 100; + public static final int LED_COLOUR = 0; + + + public static final String SCHEDULE_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ssZ"; + + public static final String PREF_USER_TOKEN = "PREF_USER_TOKEN"; + public static final String PREF_USER_ID = "PREF_USER_ID"; + public static final String PREF_USER_INSTANCE = "PREF_USER_INSTANCE"; + public static final String PREF_IS_MODERATOR = "PREF_IS_MODERATOR"; + public static final String PREF_IS_ADMINISTRATOR = "PREF_IS_ADMINISTRATOR"; + public static final String PREF_KEY_ID = "PREF_KEY_ID"; + public static final String PREF_INSTANCE = "PREF_INSTANCE"; + + + public static final int NOTIFICATION_INTENT = 1; + public static final String INTENT_TARGETED_ACCOUNT = "INTENT_TARGETED_ACCOUNT"; + + public static final String TEMP_MEDIA_DIRECTORY = "TEMP_MEDIA_DIRECTORY"; + + + public static final int EXTERNAL_STORAGE_REQUEST_CODE = 84; + public static final int EXTERNAL_STORAGE_REQUEST_CODE_MEDIA_SAVE = 85; + public static final int EXTERNAL_STORAGE_REQUEST_CODE_MEDIA_SHARE = 86; + //Some regex + /*public static final Pattern urlPattern = Pattern.compile( + "(?i)\\b((?:[a-z][\\w-]+:(?:/{1,3}|[a-z0-9%])|www\\d{0,3}[.]|[a-z0-9.\\-]+[.][a-z]{2,10}/)(?:[^\\s()<>]+|\\(([^\\s()<>]+|(\\([^\\s()<>]+\\)))*\\))+(?:\\(([^\\s()<>]+|(\\([^\\s()<>]+\\)))*\\)|[^\\s`!()\\[\\]{};:'\".,<>?«»“”‘’]))", + + Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);*/ + + public static final Pattern hashtagPattern = Pattern.compile("(#[\\w_A-zÀ-ÿ]+)"); + public static final Pattern groupPattern = Pattern.compile("(![\\w_]+)"); + public static final Pattern mentionPattern = Pattern.compile("(@[\\w_]+)"); + public static final Pattern mentionLongPattern = Pattern.compile("(@[\\w_-]+@[a-zA-Z0-9][a-zA-Z0-9-]{1,61}[a-zA-Z0-9](?:\\.[a-zA-Z]{2,})+)"); + + public static final Pattern twitterPattern = Pattern.compile("((@[\\w]+)@twitter\\.com)"); + public static final Pattern youtubePattern = Pattern.compile("(www\\.|m\\.)?(youtube\\.com|youtu\\.be|youtube-nocookie\\.com)/(((?!([\"'<])).)*)"); + public static final Pattern nitterPattern = Pattern.compile("(mobile\\.|www\\.)?twitter.com([\\w-/]+)"); + public static final Pattern bibliogramPattern = Pattern.compile("(m\\.|www\\.)?instagram.com(/p/[\\w-/]+)"); + public static final Pattern libredditPattern = Pattern.compile("(www\\.|m\\.)?(reddit\\.com|preview\\.redd\\.it|i\\.redd\\.it|redd\\.it)/(((?!([\"'<])).)*)"); + public static final Pattern ouichesPattern = Pattern.compile("https?://ouich\\.es/tag/(\\w+)"); + public static final Pattern xmppPattern = Pattern.compile("xmpp:[-a-zA-Z0-9+$&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]"); + public static final Pattern mediumPattern = Pattern.compile("([\\w@-]*)?\\.?medium.com/@?([/\\w-]+)"); + public static final Pattern wikipediaPattern = Pattern.compile("([\\w_-]+)\\.wikipedia.org/(((?!([\"'<])).)*)"); + // --- Static Map of patterns used in spannable status content + public static final Map patternHashMap; + public static int counter = 1; + + static { + Map aMap = new HashMap<>(); + aMap.put(PatternType.MENTION, mentionPattern); + aMap.put(PatternType.MENTION_LONG, mentionLongPattern); + aMap.put(PatternType.TAG, hashtagPattern); + aMap.put(PatternType.GROUP, groupPattern); + patternHashMap = Collections.unmodifiableMap(aMap); + } + + /*** + * Initialize a CustomWebview + * @param activity Activity - activity containing the webview + * @param webviewId int - webview id + * @param rootView View - the root view that will contain the webview + * @return {@link CustomWebview} + */ + public static CustomWebview initializeWebview(Activity activity, int webviewId, View rootView) { + + CustomWebview webView; + if (rootView == null) { + webView = activity.findViewById(webviewId); + } else { + webView = rootView.findViewById(webviewId); + } + final SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(activity); + boolean javascript = sharedpreferences.getBoolean(activity.getString(R.string.SET_JAVASCRIPT), true); + + webView.getSettings().setJavaScriptEnabled(javascript); + webView.getSettings().setUseWideViewPort(true); + webView.getSettings().setLoadWithOverviewMode(true); + webView.getSettings().setSupportZoom(true); + webView.getSettings().setDisplayZoomControls(false); + webView.getSettings().setBuiltInZoomControls(true); + webView.getSettings().setAllowContentAccess(true); + webView.getSettings().setLoadsImagesAutomatically(true); + webView.getSettings().setSupportMultipleWindows(false); + webView.getSettings().setMediaPlaybackRequiresUserGesture(true); + String user_agent = sharedpreferences.getString(activity.getString(R.string.SET_CUSTOM_USER_AGENT), USER_AGENT); + webView.getSettings().setUserAgentString(user_agent); + boolean cookies = sharedpreferences.getBoolean(activity.getString(R.string.SET_COOKIES), false); + CookieManager cookieManager = CookieManager.getInstance(); + cookieManager.setAcceptThirdPartyCookies(webView, cookies); + webView.setBackgroundColor(Color.TRANSPARENT); + webView.getSettings().setAppCacheEnabled(true); + webView.getSettings().setDatabaseEnabled(true); + webView.getSettings().setCacheMode(WebSettings.LOAD_DEFAULT); + webView.setWebChromeClient(new WebChromeClient() { + @Override + public Bitmap getDefaultVideoPoster() { + return Bitmap.createBitmap(50, 50, Bitmap.Config.ARGB_8888); + } + }); + boolean proxyEnabled = sharedpreferences.getBoolean(activity.getString(R.string.SET_PROXY_ENABLED), false); + if (proxyEnabled) { + String host = sharedpreferences.getString(activity.getString(R.string.SET_PROXY_HOST), "127.0.0.1"); + int port = sharedpreferences.getInt(activity.getString(R.string.SET_PROXY_PORT), 8118); + setProxy(activity, webView, host, port, WebviewActivity.class.getName()); + } + + return webView; + } + + /** + * Manage downloads with URLs + * + * @param context Context + * @param url String download url + */ + public static void manageDownloads(final Context context, final String url) { + final AlertDialog.Builder builder = new AlertDialog.Builder(context, Helper.dialogStyle()); + final DownloadManager.Request request; + try { + request = new DownloadManager.Request(Uri.parse(url.trim())); + } catch (Exception e) { + Toasty.error(context, context.getString(R.string.toast_error), Toast.LENGTH_LONG).show(); + return; + } + final String fileName = URLUtil.guessFileName(url, null, null); + builder.setMessage(context.getResources().getString(R.string.download_file, fileName)); + builder.setCancelable(false) + .setPositiveButton(context.getString(R.string.yes), (dialog, id) -> { + request.allowScanningByMediaScanner(); + request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName); + request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED); + DownloadManager dm = (DownloadManager) context.getSystemService(DOWNLOAD_SERVICE); + assert dm != null; + dm.enqueue(request); + dialog.dismiss(); + }) + .setNegativeButton(context.getString(R.string.cancel), (dialog, id) -> dialog.cancel()); + AlertDialog alert = builder.create(); + if (alert.getWindow() != null) + alert.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE); + alert.show(); + } + + public static void colorizeIconMenu(Menu menu, int toolbarIconsColor) { + final PorterDuffColorFilter colorFilter + = new PorterDuffColorFilter(toolbarIconsColor, PorterDuff.Mode.MULTIPLY); + for (int i = 0; i < menu.size(); i++) { + MenuItem v = menu.getItem(i); + v.getIcon().setColorFilter(colorFilter); + } + } + + public static void installProvider() { + + /* boolean patch_provider = true; + try { + Context ctx = MainApplication.getApp(); + SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(ctx); + patch_provider = sharedpreferences.getBoolean(Helper.SET_SECURITY_PROVIDER, true); + } catch (Exception ignored) { + } + if (patch_provider) { + try { + Security.insertProviderAt(Conscrypt.newProvider(), 1); + } catch (Exception ignored) { + } + }*/ + } + + /*** + * Check if the user is connected to Internet + * @return boolean + */ + public static BaseMainActivity.status isConnectedToInternet(Context context, String instance) { + ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + if (cm == null) + return BaseMainActivity.status.CONNECTED; + NetworkInfo ni = cm.getActiveNetworkInfo(); + if (ni != null && ni.isConnected()) { + try { + InetAddress ipAddr = InetAddress.getByName(instance); + return (!ipAddr.toString().equals("")) ? BaseMainActivity.status.CONNECTED : BaseMainActivity.status.DISCONNECTED; + } catch (Exception e) { + try { + InetAddress ipAddr = InetAddress.getByName("mastodon.social"); + return (!ipAddr.toString().equals("")) ? BaseMainActivity.status.CONNECTED : BaseMainActivity.status.DISCONNECTED; + } catch (Exception ex) { + return BaseMainActivity.status.DISCONNECTED; + } + } + } else { + return BaseMainActivity.status.DISCONNECTED; + } + } + + /** + * Returns boolean depending if the user is authenticated + * + * @param context Context + * @return boolean + */ + public static boolean isLoggedIn(Context context) { + SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(context); + String prefKeyOauthTokenT = sharedpreferences.getString(PREF_USER_TOKEN, null); + return (prefKeyOauthTokenT != null); + } + + /*** + * Returns a String depending of the date + * @param context Context + * @param date Date + * @return String + */ + public static String dateDiff(Context context, Date date) { + Date now = new Date(); + long diff = now.getTime() - date.getTime(); + long seconds = diff / 1000; + long minutes = seconds / 60; + long hours = minutes / 60; + long days = hours / 24; + long months = days / 30; + long years = days / 365; + + String format = DateFormat.getDateInstance(DateFormat.SHORT).format(date); + if (years > 0) { + return format; + } else if (months > 0 || days > 7) { + //Removes the year depending of the locale from DateFormat.SHORT format + SimpleDateFormat df = (SimpleDateFormat) DateFormat.getDateInstance(DateFormat.SHORT, Locale.getDefault()); + df.applyPattern(df.toPattern().replaceAll("[^\\p{Alpha}]*y+[^\\p{Alpha}]*", "")); + return df.format(date); + } else if (days > 0) + return context.getString(R.string.date_day, days); + else if (hours > 0) + return context.getResources().getString(R.string.date_hours, (int) hours); + else if (minutes > 0) + return context.getResources().getString(R.string.date_minutes, (int) minutes); + else { + if (seconds < 0) + seconds = 0; + return context.getResources().getString(R.string.date_seconds, (int) seconds); + } + } + + /** + * Convert a date in String -> format yyyy-MM-dd HH:mm:ss + * + * @param date Date + * @return String + */ + public static String dateToString(Date date) { + if (date == null) + return null; + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()); + return dateFormat.format(date); + } + + /** + * Convert a date in String + * + * @param date Date + * @return String + */ + public static String longDateToString(Date date) { + DateFormat df = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.SHORT, Locale.getDefault()); + return df.format(date); + } + + /** + * Convert a date in String -> format yyyy-MM-dd HH:mm:ss + * + * @param date Date + * @return String + */ + public static String shortDateToString(Date date) { + if (date == null) { + date = new Date(); + } + SimpleDateFormat df = (SimpleDateFormat) DateFormat.getDateInstance(DateFormat.SHORT, Locale.getDefault()); + return df.format(date); + } + + /** + * Convert String date from db to Date Object + * + * @param stringDate date to convert + * @return Date + */ + public static Date stringToDate(Context context, String stringDate) { + if (stringDate == null) + return null; + Locale userLocale; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + userLocale = context.getResources().getConfiguration().getLocales().get(0); + } else { + userLocale = context.getResources().getConfiguration().locale; + } + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", userLocale); + Date date = null; + try { + date = dateFormat.parse(stringDate); + } catch (java.text.ParseException ignored) { + + } + return date; + } + + /** + * Log out the authenticated user by removing its token + * + * @param activity Activity + * @param account {@link Account} + * @throws DBException Exception + */ + public static void removeAccount(Activity activity, Account account) throws DBException { + SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(activity); + //Current user + SQLiteDatabase db = Sqlite.getInstance(activity.getApplicationContext(), Sqlite.DB_NAME, null, Sqlite.DB_VERSION).open(); + String userId = sharedpreferences.getString(PREF_USER_ID, null); + String instance = sharedpreferences.getString(PREF_USER_INSTANCE, null); + Account accountDB = new Account(activity); + boolean accountRemovedIsLogged = false; + //Remove the current account + if (account == null) { + account = accountDB.getUniqAccount(userId, instance); + accountRemovedIsLogged = true; + } + if (account != null) { + Account finalAccount = account; + OauthVM oauthVM = new ViewModelProvider((ViewModelStoreOwner) activity).get(OauthVM.class); + //Revoke the token + oauthVM.revokeToken(account.instance, account.token, account.client_id, account.client_secret); + //Revoke token and remove user + new Thread(() -> { + try { + accountDB.removeUser(finalAccount); + } catch (DBException e) { + e.printStackTrace(); + } + }).start(); + } + //If the account removed is not the logged one, no need to log out the current user + if (!accountRemovedIsLogged) { + return; + } + //Log out the current user + Account newAccount = accountDB.getLastUsedAccount(); + SharedPreferences.Editor editor = sharedpreferences.edit(); + if (newAccount == null) { + editor.putString(PREF_USER_TOKEN, null); + editor.putString(PREF_USER_INSTANCE, null); + editor.putString(PREF_USER_ID, null); + editor.apply(); + Intent loginActivity = new Intent(activity, LoginActivity.class); + activity.startActivity(loginActivity); + activity.finish(); + } else { + editor.putString(PREF_USER_TOKEN, newAccount.token); + editor.putString(PREF_USER_INSTANCE, newAccount.instance); + editor.putString(PREF_USER_ID, newAccount.user_id); + BaseMainActivity.currentUserID = newAccount.user_id; + BaseMainActivity.currentToken = newAccount.token; + BaseMainActivity.currentInstance = newAccount.instance; + editor.apply(); + Intent changeAccount = new Intent(activity, BaseMainActivity.class); + changeAccount.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + activity.startActivity(changeAccount); + } + + } + + /** + * Converts dp to pixel + * + * @param dp float - the value in dp to convert + * @param context Context + * @return float - the converted value in pixel + */ + public static float convertDpToPixel(float dp, Context context) { + Resources resources = context.getResources(); + DisplayMetrics metrics = resources.getDisplayMetrics(); + return dp * ((float) metrics.densityDpi / DisplayMetrics.DENSITY_DEFAULT); + } + + /** + * Manage URLs to open (built-in or external app) + * + * @param context Context + * @param url String url to open + */ + public static void openBrowser(Context context, String url) { + url = transformURL(context, url); + SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(context); + boolean embedded_browser = sharedpreferences.getBoolean(context.getString(R.string.SET_EMBEDDED_BROWSER), true); + if (embedded_browser && !url.toLowerCase().startsWith("gemini://")) { + Intent intent = new Intent(context, WebviewActivity.class); + Bundle b = new Bundle(); + String finalUrl = url; + if (!url.toLowerCase().startsWith("http://") && !url.toLowerCase().startsWith("https://")) + finalUrl = "http://" + url; + b.putString("url", finalUrl); + intent.putExtras(b); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + } else { + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.setData(Uri.parse(url)); + try { + context.startActivity(intent); + } catch (Exception e) { + Toasty.error(context, context.getString(R.string.toast_error), Toast.LENGTH_LONG).show(); + } + } + } + + /** + * Transform URLs to privacy frontend + * + * @param context Context + * @param url String + */ + private static String transformURL(Context context, String url) { + SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(context); + Matcher matcher = Helper.nitterPattern.matcher(url); + boolean nitter = Helper.getSharedValue(context, context.getString(R.string.SET_NITTER)); + if (nitter) { + if (matcher.find()) { + final String nitter_directory = matcher.group(2); + String nitterHost = sharedpreferences.getString(context.getString(R.string.SET_NITTER_HOST), context.getString(R.string.DEFAULT_NITTER_HOST)).toLowerCase(); + return "https://" + nitterHost + nitter_directory; + } + } + matcher = Helper.bibliogramPattern.matcher(url); + boolean bibliogram = Helper.getSharedValue(context, context.getString(R.string.SET_BIBLIOGRAM)); + if (bibliogram) { + if (matcher.find()) { + final String bibliogram_directory = matcher.group(2); + String bibliogramHost = sharedpreferences.getString(context.getString(R.string.SET_BIBLIOGRAM_HOST), context.getString(R.string.DEFAULT_BIBLIOGRAM_HOST)).toLowerCase(); + return "https://" + bibliogramHost + bibliogram_directory; + } + } + matcher = Helper.libredditPattern.matcher(url); + boolean libreddit = Helper.getSharedValue(context, context.getString(R.string.SET_LIBREDDIT)); + if (libreddit) { + if (matcher.find()) { + final String libreddit_directory = matcher.group(3); + String libreddit_host = sharedpreferences.getString(context.getString(R.string.SET_LIBREDDIT_HOST), context.getString(R.string.DEFAULT_LIBREDDIT_HOST)).toLowerCase(); + return "https://" + libreddit_host + "/" + libreddit_directory; + } + } + matcher = Helper.youtubePattern.matcher(url); + boolean invidious = Helper.getSharedValue(context, context.getString(R.string.SET_INVIDIOUS)); + if (invidious) { + if (matcher.find()) { + final String youtubeId = matcher.group(3); + String invidiousHost = sharedpreferences.getString(context.getString(R.string.SET_INVIDIOUS_HOST), context.getString(R.string.DEFAULT_INVIDIOUS_HOST)).toLowerCase(); + if (matcher.group(2) != null && Objects.equals(matcher.group(2), "youtu.be")) { + return "https://" + invidiousHost + "/watch?v=" + youtubeId + "&local=true"; + } else { + return "https://" + invidiousHost + "/" + youtubeId + "&local=true"; + } + } + } + matcher = Helper.mediumPattern.matcher(url); + boolean medium = Helper.getSharedValue(context, context.getString(R.string.REPLACE_MEDIUM)); + if (medium) { + if (matcher.find()) { + String path = matcher.group(2); + String user = matcher.group(1); + if (user != null && user.length() > 0 & !user.equals("www")) { + path = user + "/" + path; + } + String mediumReplaceHost = sharedpreferences.getString(context.getString(R.string.REPLACE_MEDIUM_HOST), context.getString(R.string.DEFAULT_REPLACE_MEDIUM_HOST)).toLowerCase(); + return "https://" + mediumReplaceHost + "/" + path; + } + } + matcher = Helper.wikipediaPattern.matcher(url); + boolean wikipedia = Helper.getSharedValue(context, context.getString(R.string.REPLACE_WIKIPEDIA)); + if (wikipedia) { + if (matcher.find()) { + String subdomain = matcher.group(1); + String path = matcher.group(2); + String wikipediaReplaceHost = sharedpreferences.getString(context.getString(R.string.REPLACE_WIKIPEDIA_HOST), context.getString(R.string.DEFAULT_REPLACE_WIKIPEDIA_HOST)).toLowerCase(); + String lang = ""; + if (path != null && subdomain != null && !subdomain.equals("www")) { + lang = (path.contains("?")) ? TextUtils.htmlEncode("&") : "?"; + lang = lang + "lang=" + subdomain; + } + return "https://" + wikipediaReplaceHost + "/" + path + lang; + } + } + return url; + } + + @SuppressLint("DefaultLocale") + public static String withSuffix(long count) { + if (count < 1000) return "" + count; + int exp = (int) (Math.log(count) / Math.log(1000)); + Locale locale = null; + try { + locale = Locale.getDefault(); + } catch (Exception ignored) { + } + if (locale != null) + return String.format(locale, "%.1f %c", + count / Math.pow(1000, exp), + "kMGTPE".charAt(exp - 1)); + else + return String.format("%.1f %c", + count / Math.pow(1000, exp), + "kMGTPE".charAt(exp - 1)); + } + + /** + * Send a toast message to main activity + * + * @param context Context + * @param type String - type of the toast (error, warning, info, success) + * @param content String - message of the toast + */ + public static void sendToastMessage(Context context, String type, String content) { + Intent intentBC = new Intent(context, ToastMessage.class); + Bundle b = new Bundle(); + b.putString(RECEIVE_TOAST_TYPE, type); + b.putString(RECEIVE_TOAST_CONTENT, content); + intentBC.setAction(Helper.RECEIVE_TOAST_MESSAGE); + intentBC.putExtras(b); + context.sendBroadcast(intentBC); + } + + /** + * @param fragmentManager Fragment Manager + * @param containerViewId Id of the fragment container + * @param fragment Fragment to be added + * @param args Arguments to pass to the new fragment. null for none + * @param tag Tag to pass to the fragment + * @param backStackName An optional name to use when adding to back stack, or null. + */ + public static Fragment addFragment(@NonNull FragmentManager fragmentManager, + @IdRes int containerViewId, + @NonNull Fragment fragment, + @Nullable Bundle args, + @Nullable String tag, + @Nullable String backStackName) { + FragmentTransaction ft = fragmentManager.beginTransaction(); + ft.setCustomAnimations(R.anim.enter, R.anim.exit, R.anim.pop_enter, R.anim.pop_exit); + Fragment _fragment = fragmentManager.findFragmentByTag(tag); + if (_fragment != null && _fragment.isAdded()) { + ft.show(_fragment).commit(); + fragment = _fragment; + } else { + if (args != null) fragment.setArguments(args); + ft.add(containerViewId, fragment, tag); + if (backStackName != null) ft.addToBackStack(backStackName); + ft.commit(); + } + fragmentManager.executePendingTransactions(); + return fragment; + } + + public static int dialogStyle() { + return Cyanea.getInstance().isDark() ? R.style.DialogDark : R.style.Dialog; + } + + public static int popupStyle() { + return Cyanea.getInstance().isDark() ? R.style.PopupDark : R.style.Popup; + } + + /** + * Load a profile picture for the account + * + * @param view ImageView - the view where the image will be loaded + * @param account - {@link Account} + */ + public static void loadPP(ImageView view, Account account) { + Context context = view.getContext(); + SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(context); + boolean disableGif = sharedpreferences.getBoolean(context.getString(R.string.SET_DISABLE_GIF), false); + String targetedUrl = disableGif ? account.mastodon_account.avatar_static : account.mastodon_account.avatar; + if (disableGif || (!targetedUrl.endsWith(".gif"))) { + Glide.with(view.getContext()) + .asDrawable() + .load(targetedUrl) + .thumbnail(0.1f) + .apply(new RequestOptions().transform(new CenterCrop(), new RoundedCorners(10))) + .into(view); + } else { + Glide.with(view.getContext()) + .asGif() + .load(targetedUrl) + .thumbnail(0.1f) + .apply(new RequestOptions().transform(new CenterCrop(), new RoundedCorners(10))) + .into(view); + } + } + + /** + * Check if the app is not finishing + * + * @param context - Context + * @return boolean - context is valid and image can be loaded + */ + public static boolean isValidContextForGlide(final Context context) { + if (context == null) { + return false; + } + if (context instanceof Activity) { + final Activity activity = (Activity) context; + return !activity.isDestroyed() && !activity.isFinishing(); + } + return true; + } + + /** + * Get filename from uri + * + * @param context Context + * @param uri Uri + * @return String + */ + public static String getFileName(Context context, Uri uri) { + ContentResolver resolver = context.getContentResolver(); + Cursor returnCursor = + resolver.query(uri, null, null, null, null); + if (returnCursor != null) { + try { + int nameIndex = returnCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME); + returnCursor.moveToFirst(); + String name = returnCursor.getString(nameIndex); + returnCursor.close(); + Random r = new Random(); + int suf = r.nextInt(9999 - 1000) + 1000; + return suf + name; + } catch (Exception e) { + Random r = new Random(); + int suf = r.nextInt(9999 - 1000) + 1000; + ContentResolver cr = context.getContentResolver(); + String mime = cr.getType(uri); + if (mime != null && mime.split("/").length > 1) + return "__" + suf + "." + mime.split("/")[1]; + else + return "__" + suf + ".jpg"; + } + } else { + Random r = new Random(); + int suf = r.nextInt(9999 - 1000) + 1000; + ContentResolver cr = context.getContentResolver(); + String mime = cr.getType(uri); + if (mime != null && mime.split("/").length > 1) + return "__" + suf + "." + mime.split("/")[1]; + else + return "__" + suf + ".jpg"; + } + } + + /** + * Return shared value + * + * @param context Context + * @param type String + * @return boolean + */ + public static boolean getSharedValue(Context context, String type) { + SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(context); + if (type.compareTo(context.getString(R.string.SET_INVIDIOUS)) == 0) { + return sharedpreferences.getBoolean(type, false); + } else if (type.compareTo(context.getString(R.string.SET_BIBLIOGRAM)) == 0) { + return sharedpreferences.getBoolean(type, false); + } else if (type.compareTo(context.getString(R.string.SET_NITTER)) == 0) { + return sharedpreferences.getBoolean(type, false); + } else if (type.compareTo(context.getString(R.string.REPLACE_MEDIUM)) == 0) { + return sharedpreferences.getBoolean(type, false); + } else if (type.compareTo(context.getString(R.string.REPLACE_WIKIPEDIA)) == 0) { + return sharedpreferences.getBoolean(type, false); + } + return sharedpreferences.getBoolean(type, false); + } + + /** + * Get size from uri + * + * @param context Context + * @param uri Uri - uri to check + * @return long - file size + */ + public static long getRealSizeFromUri(Context context, Uri uri) { + Cursor cursor = null; + try { + String[] proj = {MediaStore.Audio.Media.SIZE}; + cursor = context.getContentResolver().query(uri, proj, null, null, null); + int column_index = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.SIZE); + cursor.moveToFirst(); + return Long.parseLong(cursor.getString(column_index)); + } catch (Exception e) { + return 0; + } finally { + if (cursor != null) { + cursor.close(); + } + } + + } + + /** + * Sends notification with intent + * + * @param context Context + * @param intent Intent associated to the notifcation + * @param icon Bitmap profile picture + * @param title String title of the notification + * @param message String message for the notification + */ + @SuppressLint("UnspecifiedImmutableFlag") + public static void notify_user(Context context, Account account, Intent intent, Bitmap icon, NotifType notifType, String title, String message) { + final SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(context); + // prepare intent which is triggered if the user click on the notification + NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context); + int notificationId = (int) System.currentTimeMillis(); + PendingIntent pIntent; + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { + pIntent = PendingIntent.getActivity(context, notificationId, intent, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_ONE_SHOT); + } else { + pIntent = PendingIntent.getActivity(context, notificationId, intent, PendingIntent.FLAG_ONE_SHOT); + } + intent.setFlags(Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT | Intent.FLAG_ACTIVITY_CLEAR_TOP); + RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION); + // build notification + String channelId; + String channelTitle; + + switch (notifType) { + case FAV: + channelId = "channel_favourite"; + channelTitle = context.getString(R.string.channel_notif_fav); + break; + case FOLLLOW: + channelId = "channel_follow"; + channelTitle = context.getString(R.string.channel_notif_follow); + break; + case MENTION: + channelId = "channel_mention"; + channelTitle = context.getString(R.string.channel_notif_mention); + break; + case POLL: + channelId = "channel_poll"; + channelTitle = context.getString(R.string.channel_notif_poll); + break; + case BACKUP: + channelId = "channel_backup"; + channelTitle = context.getString(R.string.channel_notif_backup); + break; + case STORE: + channelId = "channel_store"; + channelTitle = context.getString(R.string.channel_notif_media); + break; + case TOOT: + channelId = "channel_status"; + channelTitle = context.getString(R.string.channel_notif_status); + break; + default: + channelId = "channel_boost"; + channelTitle = context.getString(R.string.channel_notif_boost); + } + NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(context, channelId) + .setSmallIcon(R.drawable.ic_notification).setTicker(message) + .setWhen(System.currentTimeMillis()) + .setAutoCancel(true); + if (notifType == NotifType.MENTION) { + if (message.length() > 500) { + message = message.substring(0, 499) + "…"; + } + notificationBuilder.setStyle(new NotificationCompat.BigTextStyle().bigText(message)); + } + notificationBuilder.setGroup(account.mastodon_account.acct + "@" + account.instance) + .setContentIntent(pIntent) + .setContentText(message); + int ledColour = Color.BLUE; + switch (sharedpreferences.getInt(context.getString(R.string.SET_LED_COLOUR_VAL), LED_COLOUR)) { + case 0: // BLUE + ledColour = Color.BLUE; + break; + case 1: // CYAN + ledColour = Color.CYAN; + break; + case 2: // MAGENTA + ledColour = Color.MAGENTA; + break; + case 3: // GREEN + ledColour = Color.GREEN; + break; + case 4: // RED + ledColour = Color.RED; + break; + case 5: // YELLOW + ledColour = Color.YELLOW; + break; + case 6: // WHITE + ledColour = Color.WHITE; + break; + } + + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + NotificationChannel channel; + NotificationManager mNotificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + if (sharedpreferences.getBoolean(context.getString(R.string.SET_NOTIF_SILENT), false)) { + channel = new NotificationChannel(channelId, channelTitle, NotificationManager.IMPORTANCE_LOW); + channel.setSound(null, null); + channel.setVibrationPattern(new long[]{500, 500, 500}); + channel.enableVibration(true); + channel.setLightColor(ledColour); + } else { + channel = new NotificationChannel(channelId, channelTitle, NotificationManager.IMPORTANCE_DEFAULT); + String soundUri = sharedpreferences.getString(context.getString(R.string.SET_NOTIF_SOUND), ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + context.getPackageName() + "/" + R.raw.boop); + AudioAttributes audioAttributes = new AudioAttributes.Builder() + .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) + .setUsage(AudioAttributes.USAGE_NOTIFICATION) + .build(); + channel.setSound(Uri.parse(soundUri), audioAttributes); + } + assert mNotificationManager != null; + mNotificationManager.createNotificationChannel(channel); + } else { + if (sharedpreferences.getBoolean(context.getString(R.string.SET_NOTIF_SILENT), false)) { + notificationBuilder.setVibrate(new long[]{500, 500, 500}); + } else { + String soundUri = sharedpreferences.getString(context.getString(R.string.SET_NOTIF_SOUND), ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + context.getPackageName() + "/" + R.raw.boop); + notificationBuilder.setSound(Uri.parse(soundUri)); + } + notificationBuilder.setLights(ledColour, 500, 1000); + } + notificationBuilder.setContentTitle(title); + notificationBuilder.setLargeIcon(icon); + notificationManager.notify(notificationId, notificationBuilder.build()); + + Notification summaryNotification = + new NotificationCompat.Builder(context, channelId) + .setContentTitle(title) + .setContentText(channelTitle) + .setContentIntent(pIntent) + .setLargeIcon(icon) + .setSmallIcon(R.drawable.ic_notification) + .setGroup(account.mastodon_account.acct + "@" + account.instance) + .setGroupSummary(true) + .build(); + notificationManager.notify(0, summaryNotification); + } + + /** + * Retrieves the cache size + * + * @param directory File + * @return long value in Mo + */ + public static long cacheSize(File directory) { + long length = 0; + if (directory == null || directory.length() == 0) + return -1; + for (File file : Objects.requireNonNull(directory.listFiles())) { + if (file.isFile()) { + try { + length += file.length(); + } catch (NullPointerException e) { + return -1; + } + } else { + if (!file.getName().equals("databases") && !file.getName().equals("shared_prefs")) { + length += cacheSize(file); + } + } + } + return length; + } + + public static boolean deleteDir(File dir) { + if (dir != null && dir.isDirectory()) { + String[] children = dir.list(); + assert children != null; + for (String aChildren : children) { + if (!aChildren.equals("databases") && !aChildren.equals("shared_prefs")) { + boolean success = deleteDir(new File(dir, aChildren)); + if (!success) { + return false; + } + } + } + return dir.delete(); + } else { + return dir != null && dir.isFile() && dir.delete(); + } + } + + public static Proxy getProxy(Context context) { + SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(context); + String hostVal = sharedpreferences.getString(context.getString(R.string.SET_PROXY_HOST), "127.0.0.1"); + int portVal = sharedpreferences.getInt(context.getString(R.string.SET_PROXY_PORT), 8118); + final String login = sharedpreferences.getString(context.getString(R.string.SET_PROXY_LOGIN), null); + final String pwd = sharedpreferences.getString(context.getString(R.string.SET_PROXY_PASSWORD), null); + final int type = sharedpreferences.getInt(context.getString(R.string.SET_PROXY_TYPE), 0); + boolean enable_proxy = sharedpreferences.getBoolean(context.getString(R.string.SET_PROXY_ENABLED), false); + if (!enable_proxy) { + return null; + } + Proxy proxy = new Proxy(type == 0 ? Proxy.Type.HTTP : Proxy.Type.SOCKS, + new InetSocketAddress(hostVal, portVal)); + Authenticator.setDefault(new Authenticator() { + @Override + protected PasswordAuthentication getPasswordAuthentication() { + if (getRequestingHost().equalsIgnoreCase(hostVal)) { + if (portVal == getRequestingPort()) { + return new PasswordAuthentication(login, pwd.toCharArray()); + } + } + return null; + } + }); + return proxy; + } + + /*** + * Convert Uri to byte[] + * @param context Context + * @param uri Uri + * @return byte[] + */ + public static byte[] uriToByteArray(Context context, Uri uri) { + byte[] buffer = null; + try { + InputStream iStream = context.getContentResolver().openInputStream(uri); + ByteArrayOutputStream byteBuffer = new ByteArrayOutputStream(); + int bufferSize = 1024; + buffer = new byte[bufferSize]; + int len; + while ((len = iStream.read(buffer)) != -1) { + byteBuffer.write(buffer, 0, len); + } + return byteBuffer.toByteArray(); + } catch (IOException e) { + e.printStackTrace(); + } + return buffer; + } + + /** + * Creates MultipartBody.Part from Uri + * + * @return MultipartBody.Part for the given Uri + */ + public static MultipartBody.Part getMultipartBody(@NonNull String paramName, @NonNull Attachment attachment) { + RequestBody requestFile = RequestBody.create(MediaType.parse(attachment.mimeType), new File(attachment.local_path)); + return MultipartBody.Part.createFormData(paramName, attachment.filename, requestFile); + } + + public static MultipartBody.Part getMultipartBody(Context context, @NonNull String paramName, @NonNull Uri uri) { + byte[] imageBytes = uriToByteArray(context, uri); + ContentResolver cR = context.getApplicationContext().getContentResolver(); + String mimeType = cR.getType(uri); + RequestBody requestFile = RequestBody.create(MediaType.parse(mimeType), imageBytes); + return MultipartBody.Part.createFormData(paramName, Helper.getFileName(context, uri), requestFile); + } + + public static void createAttachmentFromUri(Context context, List uris, OnAttachmentCopied callBack) { + new Thread(() -> { + for (Uri uri : uris) { + Attachment attachment = new Attachment(); + attachment.filename = Helper.getFileName(context, uri); + attachment.size = Helper.getRealSizeFromUri(context, uri); + ContentResolver cR = context.getApplicationContext().getContentResolver(); + attachment.mimeType = cR.getType(uri); + MimeTypeMap mime = MimeTypeMap.getSingleton(); + String extension = mime.getExtensionFromMimeType(cR.getType(uri)); + SimpleDateFormat formatter = new SimpleDateFormat("yyyy_MM_dd_HH_mm_ss_" + counter, Locale.getDefault()); + counter++; + Date now = new Date(); + attachment.filename = formatter.format(now) + "." + extension; + InputStream selectedFileInputStream; + try { + selectedFileInputStream = context.getContentResolver().openInputStream(uri); + if (selectedFileInputStream != null) { + final File certCacheDir = new File(context.getCacheDir(), TEMP_MEDIA_DIRECTORY); + boolean isCertCacheDirExists = certCacheDir.exists(); + if (!isCertCacheDirExists) { + isCertCacheDirExists = certCacheDir.mkdirs(); + } + if (isCertCacheDirExists) { + String filePath = certCacheDir.getAbsolutePath() + "/" + attachment.filename; + attachment.local_path = filePath; + OutputStream selectedFileOutPutStream = new FileOutputStream(filePath); + byte[] buffer = new byte[1024]; + int length; + while ((length = selectedFileInputStream.read(buffer)) > 0) { + selectedFileOutPutStream.write(buffer, 0, length); + } + selectedFileOutPutStream.flush(); + selectedFileOutPutStream.close(); + } + selectedFileInputStream.close(); + } + } catch (IOException e) { + e.printStackTrace(); + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + Runnable myRunnable = () -> callBack.onAttachmentCopied(attachment); + mainHandler.post(myRunnable); + } + }).start(); + } + + /** + * change color of a drawable + * + * @param imageView int the ImageView + * @param hexaColor example 0xffff00 + */ + public static void changeDrawableColor(Context context, ImageView imageView, int hexaColor) { + if (imageView == null) + return; + int color; + try { + color = context.getResources().getColor(hexaColor); + } catch (Resources.NotFoundException e) { + color = hexaColor; + } + imageView.setColorFilter(color); + } + + /** + * change color of a drawable + * + * @param drawable int the drawable + * @param hexaColor example 0xffff00 + */ + public static Drawable changeDrawableColor(Context context, int drawable, int hexaColor) { + Drawable mDrawable = ContextCompat.getDrawable(context, drawable); + int color; + try { + color = Color.parseColor(context.getString(hexaColor)); + } catch (Resources.NotFoundException e) { + try { + TypedValue typedValue = new TypedValue(); + Resources.Theme theme = context.getTheme(); + theme.resolveAttribute(hexaColor, typedValue, true); + color = typedValue.data; + } catch (Resources.NotFoundException ed) { + color = hexaColor; + } + } + assert mDrawable != null; + mDrawable.setColorFilter(color, PorterDuff.Mode.SRC_ATOP); + DrawableCompat.setTint(mDrawable, color); + return mDrawable; + } + + /** + * Convert a date in String -> format yyyy-MM-dd HH:mm:ss + * + * @param context Context + * @param date Date + * @return String + */ + public static String dateFileToString(Context context, Date date) { + Locale userLocale; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + userLocale = context.getResources().getConfiguration().getLocales().get(0); + } else { + userLocale = context.getResources().getConfiguration().locale; + } + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", userLocale); + return dateFormat.format(date); + } + + //Enum that described actions to replace inside a toot content + public enum PatternType { + MENTION, + MENTION_LONG, + TAG, + GROUP + } + + public enum NotifType { + FOLLLOW, + MENTION, + BOOST, + FAV, + POLL, + STATUS, + BACKUP, + STORE, + TOOT + } + + public interface OnAttachmentCopied { + void onAttachmentCopied(Attachment attachment); + } + + public static class CacheTask { + private final WeakReference contextReference; + private float cacheSize; + + public CacheTask(Context context) { + contextReference = new WeakReference<>(context); + doInBackground(); + } + + protected void doInBackground() { + new Thread(() -> { + long sizeCache = cacheSize(contextReference.get().getCacheDir().getParentFile()); + cacheSize = 0; + if (sizeCache > 0) { + cacheSize = (float) sizeCache / 1000000.0f; + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + Runnable myRunnable = () -> { + AlertDialog.Builder builder = new AlertDialog.Builder(contextReference.get(), Helper.dialogStyle()); + LayoutInflater inflater = ((BaseMainActivity) contextReference.get()).getLayoutInflater(); + View dialogView = inflater.inflate(R.layout.popup_cache, new LinearLayout(contextReference.get()), false); + TextView message = dialogView.findViewById(R.id.message); + message.setText(contextReference.get().getString(R.string.cache_message, String.format("%s %s", String.format(Locale.getDefault(), "%.2f", cacheSize), contextReference.get().getString(R.string.cache_units)))); + builder.setView(dialogView); + builder.setTitle(R.string.cache_title); + + final SwitchCompat clean_all = dialogView.findViewById(R.id.clean_all); + final float finalCacheSize = cacheSize; + builder + .setPositiveButton(R.string.clear, (dialog, which) -> new Thread(() -> { + try { + String path = Objects.requireNonNull(contextReference.get().getCacheDir().getParentFile()).getPath(); + File dir = new File(path); + if (dir.isDirectory()) { + deleteDir(dir); + } + if (clean_all.isChecked()) { + + } else { + + } + Handler mainHandler2 = new Handler(Looper.getMainLooper()); + Runnable myRunnable2 = () -> { + Toasty.success(contextReference.get(), contextReference.get().getString(R.string.toast_cache_clear, String.format("%s %s", String.format(Locale.getDefault(), "%.2f", finalCacheSize), contextReference.get().getString(R.string.cache_units))), Toast.LENGTH_LONG).show(); + dialog.dismiss(); + }; + mainHandler2.post(myRunnable2); + } catch (Exception ignored) { + } + }).start()) + .setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss()) + .setIcon(android.R.drawable.ic_dialog_alert) + .show(); + }; + mainHandler.post(myRunnable); + }).start(); + } + } + + /** + * Change locale + * + * @param activity - Activity + */ + public static void setLocale(Activity activity) { + SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(activity); + String defaultLocaleString = sharedpreferences.getString(activity.getString(R.string.SET_DEFAULT_LOCALE_NEW), null); + if (defaultLocaleString != null) { + Locale locale; + if (defaultLocaleString.equals("zh-CN")) { + locale = Locale.SIMPLIFIED_CHINESE; + } else if (defaultLocaleString.equals("zh-TW")) { + locale = Locale.TRADITIONAL_CHINESE; + } else { + locale = new Locale(defaultLocaleString); + } + Locale.setDefault(locale); + Resources resources = activity.getResources(); + Configuration config = resources.getConfiguration(); + config.setLocale(locale); + resources.updateConfiguration(config, resources.getDisplayMetrics()); + + } + } +} diff --git a/app/src/main/java/app/fedilab/android/helper/MastodonHelper.java b/app/src/main/java/app/fedilab/android/helper/MastodonHelper.java new file mode 100644 index 000000000..fe454e4ac --- /dev/null +++ b/app/src/main/java/app/fedilab/android/helper/MastodonHelper.java @@ -0,0 +1,444 @@ +package app.fedilab.android.helper; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + + +import android.app.Activity; +import android.content.Context; +import android.content.SharedPreferences; +import android.net.Uri; +import android.os.Build; +import android.util.Patterns; +import android.view.View; +import android.widget.ImageView; +import android.widget.Toast; + +import androidx.annotation.DrawableRes; +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.ViewModelProvider; +import androidx.lifecycle.ViewModelStoreOwner; +import androidx.preference.PreferenceManager; +import androidx.work.Data; +import androidx.work.OneTimeWorkRequest; + +import com.bumptech.glide.Glide; +import com.google.gson.annotations.SerializedName; + +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; + +import app.fedilab.android.BaseMainActivity; +import app.fedilab.android.R; +import app.fedilab.android.client.entities.ScheduledBoost; +import app.fedilab.android.client.mastodon.entities.Account; +import app.fedilab.android.client.mastodon.entities.Conversation; +import app.fedilab.android.client.mastodon.entities.Notification; +import app.fedilab.android.client.mastodon.entities.Pagination; +import app.fedilab.android.client.mastodon.entities.RelationShip; +import app.fedilab.android.client.mastodon.entities.ScheduledStatus; +import app.fedilab.android.client.mastodon.entities.Status; +import app.fedilab.android.databinding.DatetimePickerBinding; +import app.fedilab.android.exception.DBException; +import app.fedilab.android.jobs.ScheduleBoostWorker; +import app.fedilab.android.ui.drawer.ComposeAdapter; +import app.fedilab.android.viewmodel.mastodon.AccountsVM; +import es.dmoral.toasty.Toasty; + +public class MastodonHelper { + + public static final String CLIENT_ID = "client_id"; + public static final String REDIRECT_URI = "redirect_uri"; + public static final String RESPONSE_TYPE = "response_type"; + public static final String SCOPE = "scope"; + public static final String REDIRECT_CONTENT_WEB = "fedilab://backtofedilab"; + public static final String OAUTH_SCOPES = "read write follow push"; + public static final String OAUTH_SCOPES_ADMIN = "read write follow push admin:read admin:write"; + + public static final int ACCOUNTS_PER_CALL = 40; + public static final int STATUSES_PER_CALL = 40; + public static final int NOTIFICATIONS_PER_CALL = 30; + + + public static int accountsPerCall(Context _mContext) { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(_mContext); + return sharedPreferences.getInt(_mContext.getString(R.string.SET_ACCOUNTS_PER_CALL), ACCOUNTS_PER_CALL); + } + + public static int statusesPerCall(Context _mContext) { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(_mContext); + return sharedPreferences.getInt(_mContext.getString(R.string.SET_STATUSES_PER_CALL), STATUSES_PER_CALL); + } + + public static int notificationsPerCall(Context _mContext) { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(_mContext); + return sharedPreferences.getInt(_mContext.getString(R.string.SET_NOTIFICATIONS_PER_CALL), NOTIFICATIONS_PER_CALL); + } + + /** + * Returns authorisation URL + * + * @param instance String instance + * @param client_id String client id + * @param admin boolean - Admin scope + * @return String - Authorisation URL + */ + public static String authorizeURL(@NonNull String instance, @NonNull String client_id, boolean admin) { + String queryString = CLIENT_ID + "=" + client_id; + queryString += "&" + REDIRECT_URI + "=" + Uri.encode(REDIRECT_CONTENT_WEB); + queryString += "&" + RESPONSE_TYPE + "=code"; + if (admin) { + queryString += "&" + SCOPE + "=" + OAUTH_SCOPES_ADMIN; + } else { + queryString += "&" + SCOPE + "=" + OAUTH_SCOPES; + } + return "https://" + instance + "/oauth/authorize" + "?" + queryString; + } + + /* /** + * Retrieve pagination from header + * + * @param headers Headers + * @return Pagination + */ + /* public static Pagination getPagination(Headers headers) { + String link = headers.get("Link"); + Pagination pagination = new Pagination(); + if (link != null) { + Pattern patternMaxId = Pattern.compile("max_id=([0-9a-zA-Z]+).*"); + Matcher matcherMaxId = patternMaxId.matcher(link); + if (matcherMaxId.find()) { + pagination.max_id = matcherMaxId.group(1); + } + Pattern patternSinceId = Pattern.compile("since_id=([0-9a-zA-Z]+).*"); + Matcher matcherSinceId = patternSinceId.matcher(link); + if (matcherSinceId.find()) { + pagination.since_id = matcherSinceId.group(1); + } + Pattern patternMinId = Pattern.compile("min_id=([0-9a-zA-Z]+).*"); + Matcher matcherMinId = patternMinId.matcher(link); + if (matcherMinId.find()) { + pagination.min_id = matcherMinId.group(1); + } + } + return pagination; + }*/ + + public static Pagination getPaginationNotification(List notificationList) { + Pagination pagination = new Pagination(); + if (notificationList == null || notificationList.size() == 0) { + return pagination; + } + pagination.max_id = notificationList.get(0).id; + pagination.min_id = String.valueOf(Long.parseLong(notificationList.get(notificationList.size() - 1).id) - 1); + return pagination; + } + + public static Pagination getPaginationStatus(List statusList) { + Pagination pagination = new Pagination(); + if (statusList == null || statusList.size() == 0) { + return pagination; + } + pagination.max_id = statusList.get(0).id; + pagination.min_id = String.valueOf(Long.parseLong(statusList.get(statusList.size() - 1).id) - 1); + return pagination; + } + + public static Pagination getPaginationAccount(List accountList) { + Pagination pagination = new Pagination(); + if (accountList == null || accountList.size() == 0) { + return pagination; + } + pagination.max_id = accountList.get(0).id; + pagination.min_id = String.valueOf(Long.parseLong(accountList.get(accountList.size() - 1).id) - 1); + return pagination; + } + + public static Pagination getPaginationScheduledStatus(List scheduledStatusList) { + Pagination pagination = new Pagination(); + if (scheduledStatusList == null || scheduledStatusList.size() == 0) { + return pagination; + } + pagination.max_id = scheduledStatusList.get(0).id; + pagination.min_id = String.valueOf(Long.parseLong(scheduledStatusList.get(scheduledStatusList.size() - 1).id) - 1); + return pagination; + } + + public static Pagination getPaginationConversation(List conversationList) { + Pagination pagination = new Pagination(); + if (conversationList == null || conversationList.size() == 0) { + return pagination; + } + pagination.max_id = conversationList.get(0).id; + pagination.min_id = String.valueOf(Long.parseLong(conversationList.get(conversationList.size() - 1).id) - 1); + return pagination; + } + + public static void loadPPMastodon(ImageView view, app.fedilab.android.client.mastodon.entities.Account account) { + loadProfileMediaMastodon(view, account, MediaAccountType.AVATAR); + } + + public static void loadProfileMediaMastodon(ImageView view, app.fedilab.android.client.mastodon.entities.Account account, MediaAccountType type) { + Context context = view.getContext(); + SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(context); + boolean disableGif = sharedpreferences.getBoolean(context.getString(R.string.SET_DISABLE_GIF), false); + String targetedUrl = disableGif ? (type == MediaAccountType.AVATAR ? account.avatar_static : account.header_static) : (type == MediaAccountType.AVATAR ? account.avatar : account.header); + @DrawableRes int placeholder = type == MediaAccountType.AVATAR ? R.drawable.ic_person : R.drawable.default_banner; + if (disableGif || (!targetedUrl.endsWith(".gif"))) { + Glide.with(view.getContext()) + .asDrawable() + .load(targetedUrl) + .thumbnail(0.1f) + .placeholder(placeholder) + .into(view); + } else { + Glide.with(view.getContext()) + .asGif() + .load(targetedUrl) + .thumbnail(0.1f) + .placeholder(placeholder) + .into(view); + } + } + + /** + * Convert a date in String -> format yyyy-MM-dd HH:mm:ss + * + * @param date Date + * @return String + */ + public static String dateToStringPoll(Date date) { + if (date == null) + return null; + SimpleDateFormat dateFormat = new SimpleDateFormat("MM-dd HH:mm", Locale.getDefault()); + return dateFormat.format(date); + } + + /*** + * Returns a String depending of the date + * @param context Context + * @param dateEndPoll Date + * @return String + */ + public static String dateDiffPoll(Context context, Date dateEndPoll) { + if (dateEndPoll == null) { + return ""; + } + Date now = new Date(); + long diff = dateEndPoll.getTime() - now.getTime(); + long seconds = diff / 1000; + long minutes = seconds / 60; + long hours = minutes / 60; + long days = hours / 24; + + if (days > 0) + return context.getResources().getQuantityString(R.plurals.date_day_polls, (int) days, (int) days); + else if (hours > 0) + return context.getResources().getQuantityString(R.plurals.date_hours_polls, (int) hours, (int) hours); + else if (minutes > 0) + return context.getResources().getQuantityString(R.plurals.date_minutes_polls, (int) minutes, (int) minutes); + else { + if (seconds < 0) + seconds = 0; + return context.getResources().getQuantityString(R.plurals.date_seconds_polls, (int) seconds, (int) seconds); + } + } + + /*** + * Returns the length used when composing a toot + * @param composeViewHolder ComposeAdapter.ComposeViewHolder itemHolder for compose elements + * @return int - characters used + */ + public static int countLength(ComposeAdapter.ComposeViewHolder composeViewHolder) { + String content = composeViewHolder.binding.content.getText().toString(); + String cwContent = composeViewHolder.binding.contentSpoiler.getText().toString(); + String contentCount = content; + contentCount = contentCount.replaceAll("(?i)(^|[^/\\w])@(([a-z0-9_]+)@[a-z0-9.-]+[a-z0-9]+)", "$1@$3"); + Matcher matcherALink = Patterns.WEB_URL.matcher(contentCount); + while (matcherALink.find()) { + final String url = matcherALink.group(1); + assert url != null; + contentCount = contentCount.replace(url, "abcdefghijklmnopkrstuvw"); + } + int contentLength = contentCount.length() - countWithEmoji(content); + int cwLength = cwContent.length() - countWithEmoji(cwContent); + return cwLength + contentLength; + } + + /** + * Length used by emoji displayed on the toot + * + * @param text String - The current text + * @return int - Number of characters used by emoji + */ + private static int countWithEmoji(String text) { + int emojiCount = 0; + for (int i = 0; i < text.length(); i++) { + int type = Character.getType(text.charAt(i)); + if (type == Character.SURROGATE || type == Character.OTHER_SYMBOL) { + emojiCount++; + } + } + return emojiCount / 2; + } + + + /** + * Schedule a boost or timed mutes + * + * @param context Context + * @param status {@link Status} + */ + public static void scheduleBoost(Context context, ScheduleType scheduleType, Status status, Account account, TimedMuted listener) { + AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(context, Helper.dialogStyle()); + DatetimePickerBinding binding = DatetimePickerBinding.inflate(((Activity) context).getLayoutInflater()); + dialogBuilder.setView(binding.getRoot()); + final AlertDialog alertDialogBoost = dialogBuilder.create(); + binding.timePicker.setIs24HourView(true); + //Buttons management + binding.dateTimeCancel.setOnClickListener(v -> alertDialogBoost.dismiss()); + binding.dateTimeNext.setOnClickListener(v -> { + binding.datePicker.setVisibility(View.GONE); + binding.timePicker.setVisibility(View.VISIBLE); + binding.dateTimePrevious.setVisibility(View.VISIBLE); + binding.dateTimeNext.setVisibility(View.GONE); + binding.dateTimeSet.setVisibility(View.VISIBLE); + }); + binding.dateTimePrevious.setOnClickListener(v -> { + binding.datePicker.setVisibility(View.VISIBLE); + binding.timePicker.setVisibility(View.GONE); + binding.dateTimePrevious.setVisibility(View.GONE); + binding.dateTimeNext.setVisibility(View.VISIBLE); + binding.dateTimeSet.setVisibility(View.GONE); + }); + binding.dateTimeSet.setOnClickListener(v -> { + int hour, minute; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + hour = binding.timePicker.getHour(); + minute = binding.timePicker.getMinute(); + } else { + hour = binding.timePicker.getCurrentHour(); + minute = binding.timePicker.getCurrentMinute(); + } + Calendar calendar = new GregorianCalendar(binding.datePicker.getYear(), + binding.datePicker.getMonth(), + binding.datePicker.getDayOfMonth(), + hour, + minute); + long time = calendar.getTimeInMillis(); + if ((time - new Date().getTime()) < 60000) { + if (scheduleType == ScheduleType.BOOST) { + Toasty.warning(context, context.getString(R.string.toot_scheduled_date), Toast.LENGTH_LONG).show(); + } else { + Toasty.warning(context, context.getString(R.string.timed_mute_date_error), Toast.LENGTH_LONG).show(); + } + } else { + //Schedules the toot + + long delayToPass = (time - new Date().getTime()); + if (scheduleType == ScheduleType.BOOST) { + Data inputData = new Data.Builder() + .putString(Helper.ARG_INSTANCE, BaseMainActivity.currentInstance) + .putString(Helper.ARG_TOKEN, BaseMainActivity.currentToken) + .putString(Helper.ARG_USER_ID, BaseMainActivity.currentUserID) + .putString(Helper.ARG_STATUS_ID, status.reblog != null ? status.reblog.id : status.id) + .build(); + OneTimeWorkRequest oneTimeWorkRequest = new OneTimeWorkRequest.Builder(ScheduleBoostWorker.class) + .setInputData(inputData) + .setInitialDelay(delayToPass, TimeUnit.MILLISECONDS) + .build(); + ScheduledBoost scheduledBoost = new ScheduledBoost(); + scheduledBoost.userId = BaseMainActivity.currentUserID; + scheduledBoost.statusId = status.reblog != null ? status.reblog.id : status.id; + scheduledBoost.scheduledAt = calendar.getTime(); + scheduledBoost.instance = BaseMainActivity.currentInstance; + scheduledBoost.workerUuid = oneTimeWorkRequest.getId(); + scheduledBoost.status = status.reblog != null ? status.reblog : status; + try { + new ScheduledBoost(context).insertScheduledBoost(scheduledBoost); + } catch (DBException e) { + e.printStackTrace(); + } + //Clear content + Toasty.info(context, context.getString(R.string.boost_scheduled), Toast.LENGTH_LONG).show(); + } else { + AccountsVM accountsVM = new ViewModelProvider((ViewModelStoreOwner) context).get(AccountsVM.class); + String accountId; + String acct; + if (account == null) { + accountId = status.account.id; + acct = status.account.acct; + } else { + accountId = account.id; + acct = account.acct; + } + accountsVM.mute(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, accountId, true, (int) delayToPass) + .observe((LifecycleOwner) context, relationShip -> { + if (listener != null) { + listener.onTimedMute(relationShip); + } + }); + Toasty.info(context, context.getString(R.string.timed_mute_date, acct, Helper.dateToString(calendar.getTime())), Toast.LENGTH_LONG).show(); + } + alertDialogBoost.dismiss(); + } + }); + alertDialogBoost.show(); + } + + public enum MediaAccountType { + AVATAR, + HEADER + } + + public enum visibility { + @SerializedName("PUBLIC") + PUBLIC("public"), + @SerializedName("UNLISTED") + UNLISTED("unlisted"), + @SerializedName("PRIVATE") + PRIVATE("private"), + @SerializedName("DIRECT") + DIRECT("direct"); + + private final String value; + + visibility(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + } + + + public enum ScheduleType { + BOOST, + TIMED_MUTED + } + + public interface TimedMuted { + void onTimedMute(RelationShip relationShip); + } + +} diff --git a/app/src/main/java/app/fedilab/android/helper/MediaHelper.java b/app/src/main/java/app/fedilab/android/helper/MediaHelper.java new file mode 100644 index 000000000..08daff5e2 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/helper/MediaHelper.java @@ -0,0 +1,403 @@ +package app.fedilab.android.helper; +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import static android.content.Context.DOWNLOAD_SERVICE; +import static app.fedilab.android.helper.Helper.notify_user; + +import android.app.Activity; +import android.app.DownloadManager; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.graphics.BitmapFactory; +import android.graphics.drawable.Drawable; +import android.media.AudioFormat; +import android.media.MediaRecorder; +import android.net.Uri; +import android.os.Build; +import android.os.Environment; +import android.provider.MediaStore; +import android.text.format.DateFormat; +import android.view.View; +import android.webkit.MimeTypeMap; +import android.webkit.URLUtil; +import android.widget.Toast; + +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.core.content.FileProvider; +import androidx.preference.PreferenceManager; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.request.target.CustomTarget; +import com.bumptech.glide.request.transition.Transition; +import com.github.piasy.rxandroidaudio.AudioRecorder; + +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.channels.FileChannel; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.Locale; +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.atomic.AtomicInteger; + +import app.fedilab.android.BaseMainActivity; +import app.fedilab.android.BuildConfig; +import app.fedilab.android.R; +import app.fedilab.android.activities.ComposeActivity; +import app.fedilab.android.databinding.DatetimePickerBinding; +import app.fedilab.android.databinding.PopupRecordBinding; +import es.dmoral.toasty.Toasty; + +public class MediaHelper { + + + /** + * Manage downloads with URLs, does not concern images, they are moved with Glide cache. + * + * @param context Context + * @param url String download url + */ + public static long manageDownloadsNoPopup(final Context context, final String url) { + final SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(context); + + final DownloadManager.Request request; + try { + request = new DownloadManager.Request(Uri.parse(url.trim())); + } catch (Exception e) { + Toasty.error(context, context.getString(R.string.toast_error), Toast.LENGTH_LONG).show(); + return -1; + } + try { + String mime = getMimeType(url); + + final String fileName = URLUtil.guessFileName(url, null, null); + request.allowScanningByMediaScanner(); + String myDir; + if (mime.toLowerCase().startsWith("video")) { + myDir = Environment.DIRECTORY_MOVIES + "/" + context.getString(R.string.app_name); + } else if (mime.toLowerCase().startsWith("audio")) { + myDir = Environment.DIRECTORY_MUSIC + "/" + context.getString(R.string.app_name); + } else { + myDir = Environment.DIRECTORY_DOWNLOADS; + } + + if (!new File(myDir).exists()) { + new File(myDir).mkdir(); + } + if (mime.toLowerCase().startsWith("video")) { + request.setDestinationInExternalPublicDir(Environment.DIRECTORY_MOVIES, context.getString(R.string.app_name) + "/" + fileName); + } else if (mime.toLowerCase().startsWith("audio")) { + request.setDestinationInExternalPublicDir(Environment.DIRECTORY_MUSIC, context.getString(R.string.app_name) + "/" + fileName); + } else { + request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName); + } + + request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED); + DownloadManager dm = (DownloadManager) context.getSystemService(DOWNLOAD_SERVICE); + return dm.enqueue(request); + } catch (Exception e) { + Toasty.error(context, context.getString(R.string.error_destination_path), Toast.LENGTH_LONG).show(); + e.printStackTrace(); + return -1; + } + } + + + /** + * Download from Glid cache + * + * @param context Context + * @param url String + */ + public static void manageMove(Context context, String url, boolean share) { + Glide.with(context) + .asFile() + .load(url) + .into(new CustomTarget() { + @Override + public void onResourceReady(@NotNull File file, Transition transition) { + final String fileName = URLUtil.guessFileName(url, null, null); + + final String targeted_folder = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).getAbsolutePath() + "/" + context.getString(R.string.app_name); + if (!new File(targeted_folder).exists()) { + new File(targeted_folder).mkdir(); + } + FileInputStream fis = null; + FileOutputStream fos = null; + FileChannel in = null; + FileChannel out = null; + try { + File backupFile = new File(targeted_folder + "/" + fileName); + //noinspection ResultOfMethodCallIgnored + backupFile.createNewFile(); + fis = new FileInputStream(file); + fos = new FileOutputStream(backupFile); + in = fis.getChannel(); + out = fos.getChannel(); + long size = in.size(); + in.transferTo(0, size, out); + String mime = getMimeType(url); + final Intent intent = new Intent(); + intent.setAction(Intent.ACTION_VIEW); + Uri uri = Uri.fromFile(backupFile); + intent.setDataAndType(uri, mime); + if (!share) { + notify_user(context, BaseMainActivity.accountWeakReference.get(), intent, BitmapFactory.decodeResource(context.getResources(), + R.mipmap.ic_launcher), Helper.NotifType.STORE, context.getString(R.string.save_over), context.getString(R.string.download_from, fileName)); + Toasty.success(context, context.getString(R.string.save_over), Toasty.LENGTH_LONG).show(); + } else { + Intent shareIntent = new Intent(Intent.ACTION_SEND); + shareIntent.putExtra(Intent.EXTRA_STREAM, uri); + shareIntent.setType(mime); + try { + context.startActivity(shareIntent); + } catch (Exception ignored) { + } + } + } catch (Throwable e) { + e.printStackTrace(); + } finally { + try { + if (fis != null) + fis.close(); + } catch (Throwable ignore) { + } + try { + if (fos != null) + fos.close(); + } catch (Throwable ignore) { + } + try { + if (in != null && in.isOpen()) + in.close(); + } catch (Throwable ignore) { + } + + try { + if (out != null && out.isOpen()) + out.close(); + } catch (Throwable ignore) { + } + } + } + + @Override + public void onLoadCleared(@Nullable Drawable placeholder) { + + } + }); + } + + public static String formatSeconds(int seconds) { + return getTwoDecimalsValue(seconds / 3600) + ":" + + getTwoDecimalsValue(seconds / 60) + ":" + + getTwoDecimalsValue(seconds % 60); + } + + private static String getTwoDecimalsValue(int value) { + if (value >= 0 && value <= 9) { + return "0" + value; + } else { + return value + ""; + } + } + + + public static String getMimeType(String url) { + String type = null; + String extension = MimeTypeMap.getFileExtensionFromUrl(url); + if (extension != null) { + type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); + } + return type; + } + + + public static Uri dispatchTakePictureIntent(Activity activity) { + Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); + Uri photoFileUri = null; + // Ensure that there's a camera activity to handle the intent + if (takePictureIntent.resolveActivity(activity.getPackageManager()) != null) { + // Create the File where the photo should go + File photoFile = null; + try { + photoFile = createImageFile(activity); + } catch (IOException ignored) { + Toasty.error(activity, activity.getString(R.string.toot_select_image_error), Toast.LENGTH_LONG).show(); + } + // Continue only if the File was successfully created + + if (photoFile != null) { + photoFileUri = FileProvider.getUriForFile(activity, + BuildConfig.APPLICATION_ID + ".fileProvider", + photoFile); + } + takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoFileUri); + activity.startActivityForResult(takePictureIntent, ComposeActivity.TAKE_PHOTO); + } + return photoFileUri; + } + + private static File createImageFile(Context context) throws IOException { + // Create an image file name + String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.ENGLISH).format(new Date()); + String imageFileName = "JPEG_" + timeStamp + "_"; + File storageDir = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES); + File image = File.createTempFile( + imageFileName, /* prefix */ + ".jpg", /* suffix */ + storageDir /* directory */ + ); + // Save a file: path for use with ACTION_VIEW intents + String mCurrentPhotoPath = image.getAbsolutePath(); + return image; + } + + /** + * Record media + * + * @param activity Activity + * @param listener ActionRecord + */ + public static void recordAudio(Activity activity, ActionRecord listener) { + String filePath = activity.getCacheDir() + "/fedilab_recorded_audio.wav"; + AudioRecorder mAudioRecorder = AudioRecorder.getInstance(); + File mAudioFile = new File(filePath); + PopupRecordBinding binding = PopupRecordBinding.inflate(activity.getLayoutInflater()); + AlertDialog.Builder audioPopup = new AlertDialog.Builder(activity, Helper.dialogStyle()); + audioPopup.setView(binding.getRoot()); + AlertDialog alert = audioPopup.create(); + alert.show(); + Timer timer = new Timer(); + AtomicInteger count = new AtomicInteger(); + timer.scheduleAtFixedRate(new TimerTask() { + @Override + public void run() { + activity.runOnUiThread(() -> { + int value = count.getAndIncrement(); + String minutes = "00"; + String seconds; + if (value > 60) { + minutes = String.valueOf(value / 60); + seconds = String.valueOf(value % 60); + } else { + seconds = String.valueOf(value); + } + if (minutes.length() == 1) { + minutes = "0" + minutes; + } + if (seconds.length() == 1) { + seconds = "0" + seconds; + } + binding.counter.setText(String.format(Locale.getDefault(), "%s:%s", minutes, seconds)); + }); + } + }, 1000, 1000); + binding.record.setOnClickListener(v -> { + mAudioRecorder.stopRecord(); + timer.cancel(); + alert.dismiss(); + listener.onRecorded(filePath); + }); + mAudioRecorder.prepareRecord(MediaRecorder.AudioSource.MIC, + AudioFormat.ENCODING_PCM_16BIT, MediaRecorder.AudioEncoder.AAC, + 48000, + 128000, + mAudioFile); + mAudioRecorder.startRecord(); + } + + /** + * Schedule a message + * + * @param activity - Activity + * @param listener - OnSchedule + */ + public static void scheduleMessage(Activity activity, OnSchedule listener) { + AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(activity, Helper.dialogStyle()); + DatetimePickerBinding binding = DatetimePickerBinding.inflate(activity.getLayoutInflater()); + + dialogBuilder.setView(binding.getRoot()); + final AlertDialog alertDialog = dialogBuilder.create(); + + if (DateFormat.is24HourFormat(activity)) { + binding.timePicker.setIs24HourView(true); + } + //Buttons management + binding.dateTimeCancel.setOnClickListener(v -> alertDialog.dismiss()); + binding.dateTimeNext.setOnClickListener(v -> { + binding.datePicker.setVisibility(View.GONE); + binding.timePicker.setVisibility(View.VISIBLE); + binding.dateTimePrevious.setVisibility(View.VISIBLE); + binding.dateTimeNext.setVisibility(View.GONE); + binding.dateTimeSet.setVisibility(View.VISIBLE); + }); + binding.dateTimePrevious.setOnClickListener(v -> { + binding.datePicker.setVisibility(View.VISIBLE); + binding.timePicker.setVisibility(View.GONE); + binding.dateTimePrevious.setVisibility(View.GONE); + binding.dateTimeNext.setVisibility(View.VISIBLE); + binding.dateTimeSet.setVisibility(View.GONE); + }); + binding.dateTimeSet.setOnClickListener(v -> { + int hour, minute; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + hour = binding.timePicker.getHour(); + minute = binding.timePicker.getMinute(); + } else { + hour = binding.timePicker.getCurrentHour(); + minute = binding.timePicker.getCurrentMinute(); + } + Calendar calendar = new GregorianCalendar(binding.datePicker.getYear(), + binding.datePicker.getMonth(), + binding.datePicker.getDayOfMonth(), + hour, + minute); + final long[] time = {calendar.getTimeInMillis()}; + + if ((time[0] - new Date().getTime()) < 60000) { + Toasty.warning(activity, activity.getString(R.string.toot_scheduled_date), Toast.LENGTH_LONG).show(); + } else { + SimpleDateFormat sdf = new SimpleDateFormat(Helper.SCHEDULE_DATE_FORMAT, Locale.getDefault()); + String date = sdf.format(calendar.getTime()); + listener.scheduledAt(date); + alertDialog.dismiss(); + } + }); + alertDialog.show(); + } + + + //Listener for recording media + public interface ActionRecord { + void onRecorded(String file); + } + + public interface OnSchedule { + void scheduledAt(String scheduledDate); + } + + +} diff --git a/app/src/main/java/app/fedilab/android/helper/NotificationsHelper.java b/app/src/main/java/app/fedilab/android/helper/NotificationsHelper.java new file mode 100644 index 000000000..d6046c875 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/helper/NotificationsHelper.java @@ -0,0 +1,344 @@ +package app.fedilab.android.helper; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import static android.text.Html.FROM_HTML_MODE_LEGACY; +import static app.fedilab.android.helper.Helper.notify_user; + +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.os.Handler; +import android.os.Looper; +import android.text.Html; +import android.text.SpannableString; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.preference.PreferenceManager; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.load.DataSource; +import com.bumptech.glide.load.engine.GlideException; +import com.bumptech.glide.request.RequestListener; +import com.bumptech.glide.request.target.CustomTarget; +import com.bumptech.glide.request.target.Target; +import com.bumptech.glide.request.transition.Transition; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import app.fedilab.android.R; +import app.fedilab.android.activities.MainActivity; +import app.fedilab.android.client.entities.Account; +import app.fedilab.android.client.mastodon.MastodonNotificationsService; +import app.fedilab.android.client.mastodon.entities.Notification; +import app.fedilab.android.client.mastodon.entities.Notifications; +import app.fedilab.android.exception.DBException; +import okhttp3.OkHttpClient; +import retrofit2.Call; +import retrofit2.Response; +import retrofit2.Retrofit; +import retrofit2.converter.gson.GsonConverterFactory; + + +public class NotificationsHelper { + + + public static void task(Context context, String slug) throws DBException { + + SharedPreferences prefs = PreferenceManager + .getDefaultSharedPreferences(context); + String[] slugArray = slug.split("@"); + Log.v(Helper.TAG, "slug: " + slug); + Account accountDb = new Account(context).getUniqAccount(slugArray[0], slugArray[1]); + if (accountDb == null) { + return; + } + String last_notifid = prefs.getString(context.getString(R.string.LAST_NOTIFICATION_MAX_ID) + slug, null); + //Check which notifications the user wants to see + boolean notif_follow = prefs.getBoolean(context.getString(R.string.SET_NOTIF_FOLLOW), true); + boolean notif_mention = prefs.getBoolean(context.getString(R.string.SET_NOTIF_MENTION), true); + boolean notif_share = prefs.getBoolean(context.getString(R.string.SET_NOTIF_SHARE), true); + boolean notif_poll = prefs.getBoolean(context.getString(R.string.SET_NOTIF_POLL), true); + boolean notif_fav = prefs.getBoolean(context.getString(R.string.SET_NOTIF_FAVOURITE), true); + //User disagree with all notifications + if (!notif_follow && !notif_fav && !notif_mention && !notif_share && !notif_poll) + return; //Nothing is done + + MastodonNotificationsService mastodonNotificationsService = init(context, slugArray[1]); + new Thread(() -> { + Notifications notifications = new Notifications(); + Call> notificationsCall; + if (last_notifid != null) { + notificationsCall = mastodonNotificationsService.getNotifications(accountDb.token, null, null, null, last_notifid, null, 30); + } else { + notificationsCall = mastodonNotificationsService.getNotifications(accountDb.token, null, null, null, null, null, 5); + } + if (notificationsCall != null) { + try { + Response> notificationsResponse = notificationsCall.execute(); + if (notificationsResponse.isSuccessful()) { + List notFilteredNotifications = notificationsResponse.body(); + notifications.notifications = TimelineHelper.filterNotification(context.getApplicationContext(), notFilteredNotifications); + for (Notification notification : notifications.notifications) { + if (notification != null) { + notification.status = SpannableHelper.convertStatus(context.getApplicationContext(), notification.status); + } + } + notifications.pagination = MastodonHelper.getPaginationNotification(notifications.notifications); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + Runnable myRunnable = () -> onRetrieveNotifications(context, notifications, accountDb); + mainHandler.post(myRunnable); + }).start(); + + } + + + private static MastodonNotificationsService init(Context context, @NonNull String instance) { + + final OkHttpClient okHttpClient = new OkHttpClient.Builder() + .readTimeout(60, TimeUnit.SECONDS) + .connectTimeout(60, TimeUnit.SECONDS) + .proxy(Helper.getProxy(context)) + .build(); + + Retrofit retrofit = new Retrofit.Builder() + .baseUrl("https://" + instance + "/api/v1/") + .addConverterFactory(GsonConverterFactory.create()) + .client(okHttpClient) + .build(); + return retrofit.create(MastodonNotificationsService.class); + } + + public static void onRetrieveNotifications(Context context, Notifications newNotifications, final Account account) { + List notificationsReceived = newNotifications.notifications; + + if (notificationsReceived == null || notificationsReceived.size() == 0 || account == null) + return; + String key = account.user_id + "@" + account.instance; + SharedPreferences prefs = PreferenceManager + .getDefaultSharedPreferences(context); + boolean notif_follow = prefs.getBoolean(context.getString(R.string.SET_NOTIF_FOLLOW), true); + boolean notif_mention = prefs.getBoolean(context.getString(R.string.SET_NOTIF_MENTION), true); + boolean notif_share = prefs.getBoolean(context.getString(R.string.SET_NOTIF_SHARE), true); + boolean notif_poll = prefs.getBoolean(context.getString(R.string.SET_NOTIF_POLL), true); + boolean notif_fav = prefs.getBoolean(context.getString(R.string.SET_NOTIF_FAVOURITE), true); + boolean notif_status = prefs.getBoolean(context.getString(R.string.SET_NOTIF_STATUS), true); + final String max_id = prefs.getString(context.getString(R.string.LAST_NOTIFICATION_MAX_ID) + key, null); + final List notifications = new ArrayList<>(); + int pos = 0; + for (Notification notif : notificationsReceived) { + if (max_id == null || notif.id.compareTo(max_id) > 0) { + notifications.add(pos, notif); + pos++; + } + } + if (notifications.size() == 0) + return; + //No previous notifications in cache, so no notification will be sent + int newFollows = 0; + int newAdds = 0; + int newMentions = 0; + int newShare = 0; + int newPolls = 0; + int newStatus = 0; + String notificationUrl; + String message = null; + String targeted_account = null; + Helper.NotifType notifType = Helper.NotifType.MENTION; + for (Notification notification : notifications) { + switch (notification.type) { + case "mention": + notifType = Helper.NotifType.MENTION; + if (notif_mention) { + if (notification.account.display_name != null && notification.account.display_name.length() > 0) + message = String.format("%s %s", notification.account.display_name, context.getString(R.string.notif_mention)); + else + message = String.format("@%s %s", notification.account.acct, context.getString(R.string.notif_mention)); + if (notification.status != null) { + if (notification.status.spoiler_text != null && notification.status.spoiler_text.length() > 0) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + message = "\n" + new SpannableString(Html.fromHtml(notification.status.spoiler_text, FROM_HTML_MODE_LEGACY)); + else + message = "\n" + new SpannableString(Html.fromHtml(notification.status.spoiler_text)); + } else { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + message = "\n" + new SpannableString(Html.fromHtml(notification.status.content, FROM_HTML_MODE_LEGACY)); + else + message = "\n" + new SpannableString(Html.fromHtml(notification.status.content)); + } + } + newFollows++; + } + break; + case "status": + notifType = Helper.NotifType.STATUS; + if (notif_status) { + if (notification.account.display_name != null && notification.account.display_name.length() > 0) + message = String.format("%s %s", notification.account.display_name, context.getString(R.string.notif_status)); + else + message = String.format("@%s %s", notification.account.acct, context.getString(R.string.notif_status)); + if (notification.status != null) { + if (notification.status.spoiler_text != null && notification.status.spoiler_text.length() > 0) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + message = "\n" + new SpannableString(Html.fromHtml(notification.status.spoiler_text, FROM_HTML_MODE_LEGACY)); + else + message = "\n" + new SpannableString(Html.fromHtml(notification.status.spoiler_text)); + } else { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + message = "\n" + new SpannableString(Html.fromHtml(notification.status.content, FROM_HTML_MODE_LEGACY)); + else + message = "\n" + new SpannableString(Html.fromHtml(notification.status.content)); + } + } + newStatus++; + } + break; + case "reblog": + notifType = Helper.NotifType.BOOST; + if (notif_share) { + if (notification.account.display_name != null && notification.account.display_name.length() > 0) + message = String.format("%s %s", notification.account.display_name, context.getString(R.string.notif_reblog)); + else + message = String.format("@%s %s", notification.account.acct, context.getString(R.string.notif_reblog)); + newShare++; + } + break; + case "favourite": + notifType = Helper.NotifType.FAV; + if (notif_fav) { + if (notification.account.display_name != null && notification.account.display_name.length() > 0) + message = String.format("%s %s", notification.account.display_name, context.getString(R.string.notif_favourite)); + else + message = String.format("@%s %s", notification.account.acct, context.getString(R.string.notif_favourite)); + newAdds++; + } + break; + case "follow_request": + notifType = Helper.NotifType.FOLLLOW; + if (notif_follow) { + if (notification.account.display_name != null && notification.account.display_name.length() > 0) + message = String.format("%s %s", notification.account.display_name, context.getString(R.string.notif_follow_request)); + else + message = String.format("@%s %s", notification.account.acct, context.getString(R.string.notif_follow_request)); + targeted_account = notification.account.id; + newFollows++; + } + break; + case "follow": + notifType = Helper.NotifType.FOLLLOW; + if (notif_follow) { + if (notification.account.display_name != null && notification.account.display_name.length() > 0) + message = String.format("%s %s", notification.account.display_name, context.getString(R.string.notif_follow)); + else + message = String.format("@%s %s", notification.account.acct, context.getString(R.string.notif_follow)); + targeted_account = notification.account.id; + newFollows++; + } + break; + case "poll": + notifType = Helper.NotifType.POLL; + if (notif_poll) { + if (notification.account.id != null && notification.account.id.equals(MainActivity.currentUserID)) + message = context.getString(R.string.notif_poll_self); + else + message = context.getString(R.string.notif_poll); + newPolls++; + } + break; + default: + } + + } + + int allNotifCount = newFollows + newAdds + newMentions + newShare + newPolls + newStatus; + if (allNotifCount > 0) { + //Some others notification + final Intent intent = new Intent(context, MainActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); + intent.putExtra(Helper.INTENT_ACTION, Helper.NOTIFICATION_INTENT); + intent.putExtra(Helper.PREF_KEY_ID, account.user_id); + if (targeted_account != null && notifType == Helper.NotifType.FOLLLOW) + intent.putExtra(Helper.INTENT_TARGETED_ACCOUNT, targeted_account); + intent.putExtra(Helper.PREF_INSTANCE, account.instance); + notificationUrl = notifications.get(0).account.avatar; + if (notificationUrl != null) { + + Handler mainHandler = new Handler(Looper.getMainLooper()); + + final String finalNotificationUrl = notificationUrl; + Helper.NotifType finalNotifType = notifType; + String finalMessage = message; + String finalMessage1 = message; + Runnable myRunnable = () -> Glide.with(context) + .asBitmap() + .load(finalNotificationUrl) + .listener(new RequestListener() { + + @Override + public boolean onResourceReady(Bitmap resource, Object model, Target target, DataSource dataSource, boolean isFirstResource) { + return false; + } + + @Override + public boolean onLoadFailed(@Nullable GlideException e, Object model, Target target, boolean isFirstResource) { + notify_user(context, account, intent, BitmapFactory.decodeResource(context.getResources(), + R.mipmap.ic_launcher), finalNotifType, context.getString(R.string.top_notification), finalMessage1); + String lastNotif = prefs.getString(context.getString(R.string.LAST_NOTIFICATION_MAX_ID) + account.user_id + "@" + account.instance, null); + if (lastNotif == null || notifications.get(0).id.compareTo(lastNotif) > 0) { + SharedPreferences.Editor editor = prefs.edit(); + editor.putString(context.getString(R.string.LAST_NOTIFICATION_MAX_ID) + account.user_id + "@" + account.instance, notifications.get(0).id); + editor.apply(); + } + return false; + } + }) + .into(new CustomTarget() { + @Override + public void onResourceReady(@NonNull Bitmap resource, Transition transition) { + notify_user(context, account, intent, resource, finalNotifType, context.getString(R.string.top_notification), finalMessage); + String lastNotif = prefs.getString(context.getString(R.string.LAST_NOTIFICATION_MAX_ID) + account.user_id + "@" + account.instance, null); + if (lastNotif == null || notifications.get(0).id.compareTo(lastNotif) > 0) { + SharedPreferences.Editor editor = prefs.edit(); + editor.putString(context.getString(R.string.LAST_NOTIFICATION_MAX_ID) + account.user_id + "@" + account.instance, notifications.get(0).id); + editor.apply(); + } + } + + @Override + public void onLoadCleared(@Nullable Drawable placeholder) { + + } + }); + mainHandler.post(myRunnable); + + } + + } + } +} diff --git a/app/src/main/java/app/fedilab/android/helper/PinnedTimelineHelper.java b/app/src/main/java/app/fedilab/android/helper/PinnedTimelineHelper.java new file mode 100644 index 000000000..17c6dcd3f --- /dev/null +++ b/app/src/main/java/app/fedilab/android/helper/PinnedTimelineHelper.java @@ -0,0 +1,656 @@ +package app.fedilab.android.helper; +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import static app.fedilab.android.BaseMainActivity.mPageReferenceMap; +import static app.fedilab.android.ui.pageadapter.FedilabPageAdapter.BOTTOM_TIMELINE_COUNT; + +import android.content.Context; +import android.os.Bundle; +import android.view.ContextThemeWrapper; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.TextView; + +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.widget.PopupMenu; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentTransaction; +import androidx.viewpager.widget.ViewPager; + +import com.google.android.material.tabs.TabLayout; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import app.fedilab.android.BaseMainActivity; +import app.fedilab.android.R; +import app.fedilab.android.client.entities.Pinned; +import app.fedilab.android.client.entities.Timeline; +import app.fedilab.android.client.entities.app.PinnedTimeline; +import app.fedilab.android.client.entities.app.RemoteInstance; +import app.fedilab.android.client.entities.app.TagTimeline; +import app.fedilab.android.client.mastodon.entities.MastodonList; +import app.fedilab.android.databinding.ActivityMainBinding; +import app.fedilab.android.exception.DBException; +import app.fedilab.android.ui.fragment.timeline.FragmentMastodonConversation; +import app.fedilab.android.ui.fragment.timeline.FragmentMastodonTimeline; +import app.fedilab.android.ui.pageadapter.FedilabPageAdapter; + +public class PinnedTimelineHelper { + + + public static void sortPositionAsc(List pinnedTimelineList) { + //noinspection ComparatorCombinators + Collections.sort(pinnedTimelineList, (obj1, obj2) -> Integer.compare(obj1.position, obj2.position)); + } + + public static void redrawTopBarPinned(BaseMainActivity activity, ActivityMainBinding activityMainBinding, Pinned pinned, List mastodonLists) { + //Values must be initialized if there is no records in db + if (pinned == null) { + pinned = new Pinned(); + pinned.user_id = BaseMainActivity.currentUserID; + pinned.instance = BaseMainActivity.currentInstance; + } + if (pinned.pinnedTimelines == null) { + pinned.pinnedTimelines = new ArrayList<>(); + } + List pinnedTimelines = pinned.pinnedTimelines; + sortPositionAsc(pinnedTimelines); + //Check if changes occurred, if mastodonLists is null it does need, because it is the first call to draw pinned + boolean needRedraw = mastodonLists == null; + //Lists have been fetched from remote account + if (mastodonLists != null) { //Currently, needRedraw is set to false + List pinnedToRemove = new ArrayList<>(); + for (PinnedTimeline pinnedTimeline : pinned.pinnedTimelines) { + if (pinnedTimeline.type == Timeline.TimeLineEnum.LIST) { + boolean present = false; + for (MastodonList mastodonList : mastodonLists) { + if (mastodonList.id.compareTo(pinnedTimeline.mastodonList.id) == 0) { + present = true; + break; + } + } + //Needs to be removed + if (!present) { + pinnedToRemove.add(pinnedTimeline); + needRedraw = true; //Something changed, redraw must be done + Pinned finalPinned2 = pinned; + new Thread(() -> { + try { + new Pinned(activity).updatePinned(finalPinned2); + } catch (DBException e) { + e.printStackTrace(); + } + }).start(); + } + } + } + if (pinnedToRemove.size() > 0) { + pinned.pinnedTimelines.removeAll(pinnedToRemove); + } + + for (MastodonList mastodonList : mastodonLists) { + boolean present = false; + for (PinnedTimeline pinnedTimeline : pinned.pinnedTimelines) { + if (pinnedTimeline.mastodonList != null && mastodonList.id.compareTo(pinnedTimeline.mastodonList.id) == 0) { + present = true; + break; + } + } + //Needs to be added + if (!present) { + Pinned finalPinned1 = pinned; + needRedraw = true; //Something changed, redraw must be done + new Thread(() -> { + PinnedTimeline pinnedTimeline = new PinnedTimeline(); + pinnedTimeline.type = Timeline.TimeLineEnum.LIST; + pinnedTimeline.position = finalPinned1.pinnedTimelines.size(); + pinnedTimeline.mastodonList = mastodonList; + finalPinned1.pinnedTimelines.add(pinnedTimeline); + try { + boolean exist = new Pinned(activity).pinnedExist(finalPinned1); + if (exist) { + new Pinned(activity).updatePinned(finalPinned1); + } else { + new Pinned(activity).insertPinned(finalPinned1); + } + } catch (DBException e) { + e.printStackTrace(); + } + }).start(); + + } + } + } + if (!needRedraw) { //if there were no changes with list, no need to update tabs + return; + } + //Pinned tab position will start after BOTTOM_TIMELINE_COUNT (ie 5) + activityMainBinding.tabLayout.removeAllTabs(); + //Small hack to hide first tabs (they represent the item of the bottom menu) + for (int i = 0; i < BOTTOM_TIMELINE_COUNT; i++) { + activityMainBinding.tabLayout.addTab(activityMainBinding.tabLayout.newTab()); + ((ViewGroup) activityMainBinding.tabLayout.getChildAt(0)).getChildAt(i).setVisibility(View.GONE); + } + List pinnedTimelineVisibleList = new ArrayList<>(); + for (PinnedTimeline pinnedTimeline : pinned.pinnedTimelines) { + if (pinnedTimeline.displayed) { + TabLayout.Tab tab = activityMainBinding.tabLayout.newTab(); + String name = ""; + switch (pinnedTimeline.type) { + case LIST: + name = pinnedTimeline.mastodonList.title; + break; + case TAG: + name = pinnedTimeline.tagTimeline.name; + if (!name.startsWith("#")) { + name = "#" + name; + } + break; + case REMOTE: + name = pinnedTimeline.remoteInstance.host; + break; + } + TextView tv = (TextView) LayoutInflater.from(activity).inflate(R.layout.custom_tab_instance, new LinearLayout(activity), false); + tv.setText(name); + + tab.setCustomView(tv); + + activityMainBinding.tabLayout.addTab(tab); + pinnedTimelineVisibleList.add(pinnedTimeline); + } + } + + LinearLayout tabStrip = (LinearLayout) activityMainBinding.tabLayout.getChildAt(0); + for (int i = 0; i < tabStrip.getChildCount(); i++) { + // Set LongClick listener to each Tab + int finalI = i; + Pinned finalPinned = pinned; + tabStrip.getChildAt(i).setOnLongClickListener(v -> { + switch (pinnedTimelineVisibleList.get(finalI - BOTTOM_TIMELINE_COUNT).type) { + case LIST: + + break; + case TAG: + tagClick(activity, finalPinned, v, finalI); + break; + case REMOTE: + instanceClick(activity, finalPinned, v, finalI); + break; + } + return true; + }); + } + activityMainBinding.viewPager.setAdapter(null); + activityMainBinding.viewPager.clearOnPageChangeListeners(); + activityMainBinding.tabLayout.clearOnTabSelectedListeners(); + + FedilabPageAdapter fedilabPageAdapter = new FedilabPageAdapter(activity.getSupportFragmentManager(), pinned); + activityMainBinding.viewPager.setAdapter(fedilabPageAdapter); + activityMainBinding.viewPager.addOnPageChangeListener(new TabLayout.TabLayoutOnPageChangeListener(activityMainBinding.tabLayout)); + activityMainBinding.viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() { + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + + } + + @Override + public void onPageSelected(int position) { + if (position < BOTTOM_TIMELINE_COUNT) { + activityMainBinding.bottomNavView.getMenu().getItem(position).setChecked(true); + } else { + activityMainBinding.bottomNavView.getMenu().setGroupCheckable(0, true, false); + for (int i = 0; i < activityMainBinding.bottomNavView.getMenu().size(); i++) { + activityMainBinding.bottomNavView.getMenu().getItem(i).setChecked(false); + } + activityMainBinding.bottomNavView.getMenu().setGroupCheckable(0, true, true); + } + } + + @Override + public void onPageScrollStateChanged(int state) { + + } + }); + + activityMainBinding.tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() { + @Override + public void onTabSelected(TabLayout.Tab tab) { + activityMainBinding.viewPager.setCurrentItem(tab.getPosition()); + } + + @Override + public void onTabUnselected(TabLayout.Tab tab) { + } + + @Override + public void onTabReselected(TabLayout.Tab tab) { + Fragment fragment = fedilabPageAdapter.getCurrentFragment(); + if (fragment instanceof FragmentMastodonTimeline) { + ((FragmentMastodonTimeline) fragment).scrollToTop(); + } else if (fragment instanceof FragmentMastodonConversation) { + ((FragmentMastodonConversation) fragment).scrollToTop(); + } + } + }); + + } + + + /** + * Manage long clicks on Tag timelines + * + * @param context - Context of the activity + * @param pinned - {@link Pinned} + * @param view - View + * @param position - int position of the tab + */ + public static void tagClick(Context context, Pinned pinned, View view, int position) { + + PopupMenu popup = new PopupMenu(new ContextThemeWrapper(context, Helper.popupStyle()), view); + int offSetPosition = position - BOTTOM_TIMELINE_COUNT; + String tag; + TagTimeline tagTimeline = pinned.pinnedTimelines.get(offSetPosition).tagTimeline; + if (tagTimeline == null) + return; + if (tagTimeline.displayName != null) + tag = tagTimeline.displayName; + else + tag = tagTimeline.name; + popup.getMenuInflater() + .inflate(R.menu.option_tag_timeline, popup.getMenu()); + Menu menu = popup.getMenu(); + + + final MenuItem itemMediaOnly = menu.findItem(R.id.action_show_media_only); + final MenuItem itemShowNSFW = menu.findItem(R.id.action_show_nsfw); + + + final boolean[] changes = {false}; + final boolean[] mediaOnly = {false}; + final boolean[] showNSFW = {false}; + mediaOnly[0] = tagTimeline.isART; + showNSFW[0] = tagTimeline.isNSFW; + itemMediaOnly.setChecked(mediaOnly[0]); + itemShowNSFW.setChecked(showNSFW[0]); + popup.setOnDismissListener(menu1 -> { + if (changes[0]) { + if (mPageReferenceMap == null) + return; + FragmentTransaction fragTransaction = ((BaseMainActivity) context).getSupportFragmentManager().beginTransaction(); + FragmentMastodonTimeline fragmentMastodonTimeline = (FragmentMastodonTimeline) mPageReferenceMap.get(pinned.pinnedTimelines.get(position).position); + if (fragmentMastodonTimeline == null) + return; + fragTransaction.detach(fragmentMastodonTimeline); + Bundle bundle = new Bundle(); + bundle.putString("tag", tagTimeline.name); + bundle.putInt("timelineId", tagTimeline.id); + bundle.putSerializable("type", Timeline.TimeLineEnum.TAG); + if (mediaOnly[0]) + bundle.putString("instanceType", "ART"); + else + bundle.putString("instanceType", "MASTODON"); + fragmentMastodonTimeline.setArguments(bundle); + fragTransaction.attach(fragmentMastodonTimeline); + fragTransaction.commit(); + } + }); + + + popup.setOnMenuItemClickListener(item -> { + item.setShowAsAction(MenuItem.SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW); + item.setActionView(new View(context)); + item.setOnActionExpandListener(new MenuItem.OnActionExpandListener() { + @Override + public boolean onMenuItemActionExpand(MenuItem item) { + return false; + } + + @Override + public boolean onMenuItemActionCollapse(MenuItem item) { + return false; + } + }); + changes[0] = true; + int itemId = item.getItemId(); + if (itemId == R.id.action_show_media_only) { + mediaOnly[0] = !mediaOnly[0]; + tagTimeline.isART = mediaOnly[0]; + pinned.pinnedTimelines.get(offSetPosition).tagTimeline = tagTimeline; + itemMediaOnly.setChecked(mediaOnly[0]); + try { + new Pinned(context).updatePinned(pinned); + } catch (DBException e) { + e.printStackTrace(); + } + } else if (itemId == R.id.action_show_nsfw) { + showNSFW[0] = !showNSFW[0]; + tagTimeline.isNSFW = showNSFW[0]; + pinned.pinnedTimelines.get(offSetPosition).tagTimeline = tagTimeline; + itemShowNSFW.setChecked(showNSFW[0]); + try { + new Pinned(context).updatePinned(pinned); + } catch (DBException e) { + e.printStackTrace(); + } + } else if (itemId == R.id.action_any) { + AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(context, Helper.dialogStyle()); + LayoutInflater inflater = ((BaseMainActivity) context).getLayoutInflater(); + View dialogView = inflater.inflate(R.layout.tags_any, new LinearLayout(context), false); + dialogBuilder.setView(dialogView); + final EditText editText = dialogView.findViewById(R.id.filter_any); + if (tagTimeline.any != null) { + StringBuilder valuesTag = new StringBuilder(); + for (String val : tagTimeline.any) + valuesTag.append(val).append(" "); + editText.setText(valuesTag.toString()); + editText.setSelection(editText.getText().toString().length()); + } + dialogBuilder.setPositiveButton(R.string.validate, (dialog, id) -> { + String[] values = editText.getText().toString().trim().split("\\s+"); + tagTimeline.any = new ArrayList<>(Arrays.asList(values)); + pinned.pinnedTimelines.get(offSetPosition).tagTimeline = tagTimeline; + try { + new Pinned(context).updatePinned(pinned); + } catch (DBException e) { + e.printStackTrace(); + } + }); + AlertDialog alertDialog = dialogBuilder.create(); + alertDialog.show(); + } else if (itemId == R.id.action_all) { + AlertDialog.Builder dialogBuilder; + LayoutInflater inflater; + View dialogView; + AlertDialog alertDialog; + dialogBuilder = new AlertDialog.Builder(context, Helper.dialogStyle()); + inflater = ((BaseMainActivity) context).getLayoutInflater(); + dialogView = inflater.inflate(R.layout.tags_all, new LinearLayout(context), false); + dialogBuilder.setView(dialogView); + final EditText editTextAll = dialogView.findViewById(R.id.filter_all); + if (tagTimeline.all != null) { + StringBuilder valuesTag = new StringBuilder(); + for (String val : tagTimeline.all) + valuesTag.append(val).append(" "); + editTextAll.setText(valuesTag.toString()); + editTextAll.setSelection(editTextAll.getText().toString().length()); + } + dialogBuilder.setPositiveButton(R.string.validate, (dialog, id) -> { + String[] values = editTextAll.getText().toString().trim().split("\\s+"); + tagTimeline.all = new ArrayList<>(Arrays.asList(values)); + pinned.pinnedTimelines.get(offSetPosition).tagTimeline = tagTimeline; + try { + new Pinned(context).updatePinned(pinned); + } catch (DBException e) { + e.printStackTrace(); + } + }); + alertDialog = dialogBuilder.create(); + alertDialog.show(); + } else if (itemId == R.id.action_none) { + AlertDialog.Builder dialogBuilder; + LayoutInflater inflater; + View dialogView; + AlertDialog alertDialog; + dialogBuilder = new AlertDialog.Builder(context, Helper.dialogStyle()); + inflater = ((BaseMainActivity) context).getLayoutInflater(); + dialogView = inflater.inflate(R.layout.tags_all, new LinearLayout(context), false); + dialogBuilder.setView(dialogView); + final EditText editTextNone = dialogView.findViewById(R.id.filter_all); + if (tagTimeline.none != null) { + StringBuilder valuesTag = new StringBuilder(); + for (String val : tagTimeline.none) + valuesTag.append(val).append(" "); + editTextNone.setText(valuesTag.toString()); + editTextNone.setSelection(editTextNone.getText().toString().length()); + } + dialogBuilder.setPositiveButton(R.string.validate, (dialog, id) -> { + String[] values = editTextNone.getText().toString().trim().split("\\s+"); + tagTimeline.none = new ArrayList<>(Arrays.asList(values)); + pinned.pinnedTimelines.get(offSetPosition).tagTimeline = tagTimeline; + try { + new Pinned(context).updatePinned(pinned); + } catch (DBException e) { + e.printStackTrace(); + } + }); + alertDialog = dialogBuilder.create(); + alertDialog.show(); + } else if (itemId == R.id.action_displayname) { + AlertDialog.Builder dialogBuilder; + LayoutInflater inflater; + View dialogView; + AlertDialog alertDialog; + dialogBuilder = new AlertDialog.Builder(context, Helper.dialogStyle()); + inflater = ((BaseMainActivity) context).getLayoutInflater(); + dialogView = inflater.inflate(R.layout.tags_name, new LinearLayout(context), false); + dialogBuilder.setView(dialogView); + final EditText editTextName = dialogView.findViewById(R.id.column_name); + if (tagTimeline.displayName != null) { + editTextName.setText(tagTimeline.displayName); + editTextName.setSelection(editTextName.getText().toString().length()); + } + dialogBuilder.setPositiveButton(R.string.validate, (dialog, id) -> { + String values = editTextName.getText().toString(); + if (values.trim().length() == 0) + values = tag; + tagTimeline.displayName = values; + pinned.pinnedTimelines.get(offSetPosition).tagTimeline = tagTimeline; + try { + new Pinned(context).updatePinned(pinned); + } catch (DBException e) { + e.printStackTrace(); + } + }); + alertDialog = dialogBuilder.create(); + alertDialog.show(); + } + return false; + }); + popup.show(); + } + + + /** + * Manage long clicks on followed instances + * + * @param context - Context of the activity + * @param pinned - {@link Pinned} + * @param view - View + * @param position - int position of the tab + */ + public static void instanceClick(Context context, Pinned pinned, View view, int position) { + + PopupMenu popup = new PopupMenu(new ContextThemeWrapper(context, Helper.popupStyle()), view); + int offSetPosition = position - BOTTOM_TIMELINE_COUNT; + RemoteInstance remoteInstance = pinned.pinnedTimelines.get(offSetPosition).remoteInstance; + if (remoteInstance == null) + return; + final String[] currentFilter = {remoteInstance.filteredWith}; + final boolean[] changes = {false}; + String title; + if (currentFilter[0] == null) { + title = "✔ " + context.getString(R.string.all); + } else { + title = context.getString(R.string.all); + } + + MenuItem itemAll = popup.getMenu().add(0, 0, Menu.NONE, title); + + itemAll.setOnMenuItemClickListener(item -> { + item.setShowAsAction(MenuItem.SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW); + item.setActionView(new View(context)); + item.setOnActionExpandListener(new MenuItem.OnActionExpandListener() { + @Override + public boolean onMenuItemActionExpand(MenuItem item) { + return false; + } + + @Override + public boolean onMenuItemActionCollapse(MenuItem item) { + return false; + } + }); + changes[0] = true; + FragmentTransaction fragTransaction = ((BaseMainActivity) context).getSupportFragmentManager().beginTransaction(); + if (mPageReferenceMap == null) + return true; + FragmentMastodonTimeline fragmentMastodonTimeline = (FragmentMastodonTimeline) mPageReferenceMap.get(pinned.pinnedTimelines.get(position).position); + if (fragmentMastodonTimeline == null) + return false; + pinned.pinnedTimelines.get(offSetPosition).remoteInstance.filteredWith = null; + remoteInstance.filteredWith = null; + currentFilter[0] = null; + pinned.pinnedTimelines.get(offSetPosition).remoteInstance = remoteInstance; + try { + new Pinned(context).updatePinned(pinned); + } catch (DBException e) { + e.printStackTrace(); + } + fragTransaction.detach(fragmentMastodonTimeline); + Bundle bundle = new Bundle(); + bundle.putString("remote_instance", remoteInstance.host != null ? remoteInstance.host : ""); + bundle.putString("instanceType", remoteInstance.type.getValue()); + bundle.putString("timelineId", remoteInstance.id); + bundle.putSerializable("type", Timeline.TimeLineEnum.REMOTE); + fragmentMastodonTimeline.setArguments(bundle); + fragTransaction.attach(fragmentMastodonTimeline); + fragTransaction.commit(); + popup.getMenu().close(); + return false; + }); + + java.util.List tags = remoteInstance.tags; + if (tags != null && tags.size() > 0) { + java.util.Collections.sort(tags); + for (String tag : tags) { + if (tag == null || tag.length() == 0) + continue; + if (currentFilter[0] != null && currentFilter[0].equals(tag)) { + title = "✔ " + tag; + } else { + title = tag; + } + MenuItem item = popup.getMenu().add(0, 0, Menu.NONE, title); + item.setOnMenuItemClickListener(item1 -> { + FragmentTransaction fragTransaction = ((BaseMainActivity) context).getSupportFragmentManager().beginTransaction(); + if (mPageReferenceMap == null) + return true; + FragmentMastodonTimeline fragmentMastodonTimeline = (FragmentMastodonTimeline) mPageReferenceMap.get(pinned.pinnedTimelines.get(position).position); + if (fragmentMastodonTimeline == null) + return false; + pinned.pinnedTimelines.get(offSetPosition).remoteInstance.filteredWith = tag; + remoteInstance.filteredWith = tag; + try { + new Pinned(context).updatePinned(pinned); + } catch (DBException e) { + e.printStackTrace(); + } + currentFilter[0] = remoteInstance.filteredWith; + fragTransaction.detach(fragmentMastodonTimeline); + Bundle bundle = new Bundle(); + bundle.putString("remote_instance", remoteInstance.host != null ? remoteInstance.host : ""); + bundle.putString("instanceType", remoteInstance.type.getValue()); + bundle.putString("timelineId", remoteInstance.id); + bundle.putString("currentfilter", remoteInstance.filteredWith); + bundle.putSerializable("type", Timeline.TimeLineEnum.REMOTE); + fragmentMastodonTimeline.setArguments(bundle); + fragTransaction.attach(fragmentMastodonTimeline); + fragTransaction.commit(); + return false; + }); + } + } + + + MenuItem itemadd = popup.getMenu().add(0, 0, Menu.NONE, context.getString(R.string.add_tags)); + itemadd.setOnMenuItemClickListener(item -> { + item.setShowAsAction(MenuItem.SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW); + item.setActionView(new View(context)); + item.setOnActionExpandListener(new MenuItem.OnActionExpandListener() { + @Override + public boolean onMenuItemActionExpand(MenuItem item) { + return false; + } + + @Override + public boolean onMenuItemActionCollapse(MenuItem item) { + return false; + } + }); + changes[0] = true; + AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(context, Helper.dialogStyle()); + LayoutInflater inflater = ((BaseMainActivity) context).getLayoutInflater(); + View dialogView = inflater.inflate(R.layout.tags_instance, new LinearLayout(context), false); + dialogBuilder.setView(dialogView); + final EditText editText = dialogView.findViewById(R.id.filter_words); + if (remoteInstance.tags != null) { + StringBuilder valuesTag = new StringBuilder(); + for (String val : remoteInstance.tags) + valuesTag.append(val).append(" "); + editText.setText(valuesTag.toString()); + editText.setSelection(editText.getText().toString().length()); + } + dialogBuilder.setPositiveButton(R.string.validate, (dialog, id) -> { + String[] values = editText.getText().toString().trim().split("\\s+"); + remoteInstance.tags = new ArrayList<>(Arrays.asList(values)); + try { + new Pinned(context).updatePinned(pinned); + } catch (DBException e) { + e.printStackTrace(); + } + popup.getMenu().clear(); + popup.getMenu().close(); + instanceClick(context, pinned, view, offSetPosition); + }); + AlertDialog alertDialog = dialogBuilder.create(); + alertDialog.show(); + return false; + }); + + popup.setOnDismissListener(menu -> { + if (changes[0]) { + FragmentTransaction fragTransaction = ((BaseMainActivity) context).getSupportFragmentManager().beginTransaction(); + if (mPageReferenceMap == null) + return; + FragmentMastodonTimeline fragmentMastodonTimeline = (FragmentMastodonTimeline) mPageReferenceMap.get(pinned.pinnedTimelines.get(position).position); + if (fragmentMastodonTimeline == null) + return; + fragTransaction.detach(fragmentMastodonTimeline); + Bundle bundle = new Bundle(); + bundle.putString("remote_instance", remoteInstance.host != null ? remoteInstance.host : ""); + bundle.putString("instanceType", remoteInstance.type.getValue()); + bundle.putString("timelineId", remoteInstance.id); + if (currentFilter[0] != null) { + bundle.putString("currentfilter", remoteInstance.filteredWith); + } + bundle.putSerializable("type", Timeline.TimeLineEnum.REMOTE); + fragmentMastodonTimeline.setArguments(bundle); + fragTransaction.attach(fragmentMastodonTimeline); + fragTransaction.commit(); + } + }); + + popup.show(); + } +} diff --git a/app/src/main/java/app/fedilab/android/helper/PushHelper.java b/app/src/main/java/app/fedilab/android/helper/PushHelper.java new file mode 100644 index 000000000..00980b9b6 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/helper/PushHelper.java @@ -0,0 +1,133 @@ +package app.fedilab.android.helper; +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + + +import android.app.Activity; +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Handler; +import android.os.Looper; +import android.text.SpannableString; +import android.text.method.LinkMovementMethod; +import android.text.util.Linkify; +import android.widget.TextView; + +import androidx.appcompat.app.AlertDialog; +import androidx.preference.PreferenceManager; +import androidx.work.PeriodicWorkRequest; +import androidx.work.WorkManager; + +import org.unifiedpush.android.connector.UnifiedPush; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import app.fedilab.android.R; +import app.fedilab.android.client.entities.Account; +import app.fedilab.android.jobs.NotificationsWorker; + +public class PushHelper { + + public static void startStreaming(Context context) { + + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + String typeOfNotification = prefs.getString(context.getString(R.string.SET_NOTIFICATION_TYPE), "PUSH_NOTIFICATIONS"); + switch (typeOfNotification) { + case "PUSH_NOTIFICATIONS": + new Thread(() -> { + List accounts = new Account(context).getPushNotificationAccounts(); + Handler mainHandler = new Handler(Looper.getMainLooper()); + Runnable myRunnable = () -> { + List distributors = UnifiedPush.getDistributors(context, new ArrayList<>()); + if (distributors.size() == 0) { + AlertDialog.Builder alert = new AlertDialog.Builder(context, Helper.dialogStyle()); + alert.setTitle(R.string.no_distributors_found); + final TextView message = new TextView(context); + String link = "https://fedilab.app/wiki/features/push-notifications/"; + final SpannableString s = + new SpannableString(context.getString(R.string.no_distributors_explanation, link)); + Linkify.addLinks(s, Linkify.WEB_URLS); + message.setText(s); + message.setPadding(30, 20, 30, 10); + message.setMovementMethod(LinkMovementMethod.getInstance()); + alert.setView(message); + alert.setPositiveButton(R.string.close, (dialog, whichButton) -> dialog.dismiss()); + alert.show(); + } else { + registerAppWithDialog(context, accounts); + } + }; + mainHandler.post(myRunnable); + }).start(); + //Cancel scheduled jobs + WorkManager.getInstance(context).cancelAllWorkByTag(Helper.WORKER_REFRESH_NOTIFICATION); + break; + case "REPEAT_NOTIFICATIONS": + new Thread(() -> { + List accounts = new Account(context).getPushNotificationAccounts(); + for (Account account : accounts) { + ((Activity) context).runOnUiThread(() -> { + UnifiedPush.unregisterApp(context, account.user_id + "@" + account.instance); + }); + } + }).start(); + new PeriodicWorkRequest.Builder(NotificationsWorker.class, 20, TimeUnit.MINUTES) + .addTag(Helper.WORKER_REFRESH_NOTIFICATION) + .build(); + break; + case "NO_NOTIFICATIONS": + WorkManager.getInstance(context).cancelAllWorkByTag(Helper.WORKER_REFRESH_NOTIFICATION); + new Thread(() -> { + List accounts = new Account(context).getPushNotificationAccounts(); + for (Account account : accounts) { + ((Activity) context).runOnUiThread(() -> { + UnifiedPush.unregisterApp(context, account.user_id + "@" + account.instance); + }); + } + }).start(); + break; + } + } + + + private static void registerAppWithDialog(Context context, List accounts) { + + List distributors = UnifiedPush.getDistributors(context, new ArrayList<>()); + if (distributors.size() == 1 || !UnifiedPush.getDistributor(context).isEmpty()) { + if (distributors.size() == 1) { + UnifiedPush.saveDistributor(context, distributors.get(0)); + } + for (Account account : accounts) { + UnifiedPush.registerApp(context, account.user_id + "@" + account.instance, new ArrayList<>(), ""); + } + return; + } + + AlertDialog.Builder alert = new AlertDialog.Builder(context, Helper.dialogStyle()); + alert.setTitle(R.string.select_distributors); + String[] distributorsStr = distributors.toArray(new String[0]); + alert.setSingleChoiceItems(distributorsStr, -1, (dialog, item) -> { + String distributor = distributorsStr[item]; + UnifiedPush.saveDistributor(context, distributor); + for (Account account : accounts) { + UnifiedPush.registerApp(context, account.user_id + "@" + account.instance, new ArrayList<>(), ""); + } + dialog.dismiss(); + }); + alert.show(); + } +} diff --git a/app/src/main/java/app/fedilab/android/helper/PushNotifications.java b/app/src/main/java/app/fedilab/android/helper/PushNotifications.java new file mode 100644 index 000000000..e7d40f588 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/helper/PushNotifications.java @@ -0,0 +1,132 @@ +package app.fedilab.android.helper; +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + + +import static app.fedilab.android.helper.ECDH.kp_private; +import static app.fedilab.android.helper.ECDH.kp_public; + +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Handler; +import android.os.Looper; + +import androidx.annotation.NonNull; +import androidx.preference.PreferenceManager; + +import java.io.IOException; +import java.util.Random; +import java.util.concurrent.TimeUnit; + +import app.fedilab.android.BaseMainActivity; +import app.fedilab.android.R; +import app.fedilab.android.client.mastodon.MastodonNotificationsService; +import app.fedilab.android.client.mastodon.entities.PushSubscription; +import okhttp3.OkHttpClient; +import retrofit2.Call; +import retrofit2.Response; +import retrofit2.Retrofit; +import retrofit2.converter.gson.GsonConverterFactory; + + +public class PushNotifications { + + + public static void registerPushNotifications(Context context, String endpoint, String slug) { + + SharedPreferences prefs = PreferenceManager + .getDefaultSharedPreferences(context); + String strPub = prefs.getString(kp_public + slug, ""); + String strPriv = prefs.getString(kp_private + slug, ""); + ECDH ecdh = null; + try { + ecdh = ECDH.getInstance(slug); + } catch (Exception e) { + e.printStackTrace(); + } + if (ecdh == null) { + return; + } + if (strPub.trim().isEmpty() || strPriv.trim().isEmpty()) { + ecdh.newPair(context); + } + String pubKey = ecdh.getPublicKey(context); + byte[] randBytes = new byte[16]; + new Random().nextBytes(randBytes); + String auth = ECDH.base64Encode(randBytes); + + + boolean notif_follow = prefs.getBoolean(context.getString(R.string.SET_NOTIF_FOLLOW), true); + boolean notif_mention = prefs.getBoolean(context.getString(R.string.SET_NOTIF_MENTION), true); + boolean notif_share = prefs.getBoolean(context.getString(R.string.SET_NOTIF_SHARE), true); + boolean notif_poll = prefs.getBoolean(context.getString(R.string.SET_NOTIF_POLL), true); + boolean notif_fav = prefs.getBoolean(context.getString(R.string.SET_NOTIF_FAVOURITE), true); + MastodonNotificationsService mastodonNotificationsService = init(context, BaseMainActivity.currentInstance); + ECDH finalEcdh = ecdh; + new Thread(() -> { + PushSubscription pushSubscription; + Call pushSubscriptionCall = mastodonNotificationsService.pushSubscription( + BaseMainActivity.currentToken, + endpoint, + pubKey, + auth, + notif_follow, + notif_fav, + notif_share, + notif_mention, + notif_poll); + if (pushSubscriptionCall != null) { + try { + Response pushSubscriptionResponse = pushSubscriptionCall.execute(); + if (pushSubscriptionResponse.isSuccessful()) { + pushSubscription = pushSubscriptionResponse.body(); + if (pushSubscription != null) { + finalEcdh.saveServerKey(context, pushSubscription.server_key); + } + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + Runnable myRunnable = () -> { + + }; + mainHandler.post(myRunnable); + }).start(); + + + } + + public static String getToken(Context context, String slug) { + return context.getSharedPreferences("unifiedpush.connector", Context.MODE_PRIVATE).getString( + slug + "/unifiedpush.connector", null); + } + + private static MastodonNotificationsService init(@NonNull Context context, @NonNull String instance) { + final OkHttpClient okHttpClient = new OkHttpClient.Builder() + .readTimeout(60, TimeUnit.SECONDS) + .connectTimeout(60, TimeUnit.SECONDS) + .proxy(Helper.getProxy(context.getApplicationContext())) + .build(); + Retrofit retrofit = new Retrofit.Builder() + .baseUrl("https://" + instance + "/api/v1/") + .addConverterFactory(GsonConverterFactory.create()) + .client(okHttpClient) + .build(); + return retrofit.create(MastodonNotificationsService.class); + } + +} diff --git a/app/src/main/java/app/fedilab/android/helper/SpannableHelper.java b/app/src/main/java/app/fedilab/android/helper/SpannableHelper.java new file mode 100644 index 000000000..f96d28833 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/helper/SpannableHelper.java @@ -0,0 +1,613 @@ +package app.fedilab.android.helper; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + + +import static app.fedilab.android.helper.Helper.convertDpToPixel; +import static app.fedilab.android.helper.ThemeHelper.linkColor; + +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.os.Bundle; +import android.text.Html; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.TextPaint; +import android.text.style.ClickableSpan; +import android.text.style.ImageSpan; +import android.text.style.URLSpan; +import android.util.Patterns; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.preference.PreferenceManager; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.request.FutureTarget; +import com.github.penfeizhou.animation.apng.APNGDrawable; +import com.github.penfeizhou.animation.apng.decode.APNGParser; +import com.github.penfeizhou.animation.gif.GifDrawable; +import com.github.penfeizhou.animation.gif.decode.GifParser; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import app.fedilab.android.R; +import app.fedilab.android.activities.HashTagActivity; +import app.fedilab.android.activities.ProfileActivity; +import app.fedilab.android.client.mastodon.entities.Account; +import app.fedilab.android.client.mastodon.entities.Attachment; +import app.fedilab.android.client.mastodon.entities.Emoji; +import app.fedilab.android.client.mastodon.entities.Field; +import app.fedilab.android.client.mastodon.entities.Mention; +import app.fedilab.android.client.mastodon.entities.Poll; +import app.fedilab.android.client.mastodon.entities.Status; + +public class SpannableHelper { + + public static final String CLICKABLE_SPAN = "CLICKABLE_SPAN"; + + /** + * Convert HTML content to text. Also, it handles click on link and transform emoji + * This needs to be run asynchronously + * + * @param context {@link Context} + * @param status {@link Status} - Status concerned by the spannable transformation + * @param text String - text to convert, it can be content, spoiler, poll items, etc. + * @return Spannable string + */ + private static Spannable convert(@NonNull Context context, @NonNull Status status, @NonNull String text) { + SpannableString initialContent; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + initialContent = new SpannableString(Html.fromHtml(text, Html.FROM_HTML_MODE_LEGACY)); + else + initialContent = new SpannableString(Html.fromHtml(text)); + + SpannableStringBuilder content = new SpannableStringBuilder(initialContent); + URLSpan[] urls = content.getSpans(0, (content.length() - 1), URLSpan.class); + for (URLSpan span : urls) + content.removeSpan(span); + //--- EMOJI ---- + SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(context); + boolean disableGif = sharedpreferences.getBoolean(context.getString(R.string.SET_DISABLE_GIF), false); + List emojiList = status.reblog != null ? status.reblog.emojis : status.emojis; + //Will convert emoji if asked + if (emojiList != null && emojiList.size() > 0) { + for (Emoji emoji : emojiList) { + if (Helper.isValidContextForGlide(context)) { + FutureTarget futureTarget = Glide.with(context) + .asFile() + .load(disableGif ? emoji.static_url : emoji.url) + .submit(); + try { + File file = futureTarget.get(); + final String targetedEmoji = ":" + emoji.shortcode + ":"; + if (content.toString().contains(targetedEmoji)) { + //emojis can be used several times so we have to loop + for (int startPosition = -1; (startPosition = content.toString().indexOf(targetedEmoji, startPosition + 1)) != -1; startPosition++) { + final int endPosition = startPosition + targetedEmoji.length(); + if (endPosition <= content.toString().length() && endPosition >= startPosition) { + ImageSpan imageSpan; + if (APNGParser.isAPNG(file.getAbsolutePath())) { + APNGDrawable apngDrawable = APNGDrawable.fromFile(file.getAbsolutePath()); + try { + apngDrawable.setBounds(0, 0, (int) convertDpToPixel(20, context), (int) convertDpToPixel(20, context)); + apngDrawable.setVisible(true, true); + imageSpan = new ImageSpan(apngDrawable); + content.setSpan( + imageSpan, startPosition, + endPosition, Spannable.SPAN_INCLUSIVE_EXCLUSIVE); + + } catch (Exception ignored) { + } + } else if (GifParser.isGif(file.getAbsolutePath())) { + GifDrawable gifDrawable = GifDrawable.fromFile(file.getAbsolutePath()); + try { + gifDrawable.setBounds(0, 0, (int) convertDpToPixel(20, context), (int) convertDpToPixel(20, context)); + gifDrawable.setVisible(true, true); + imageSpan = new ImageSpan(gifDrawable); + content.setSpan( + imageSpan, startPosition, + endPosition, Spannable.SPAN_INCLUSIVE_EXCLUSIVE); + } catch (Exception ignored) { + } + } else { + Drawable drawable = Drawable.createFromPath(file.getAbsolutePath()); + try { + drawable.setBounds(0, 0, (int) convertDpToPixel(20, context), (int) convertDpToPixel(20, context)); + drawable.setVisible(true, true); + imageSpan = new ImageSpan(drawable); + content.setSpan( + imageSpan, startPosition, + endPosition, Spannable.SPAN_INCLUSIVE_EXCLUSIVE); + } catch (Exception ignored) { + } + } + } + } + } + } catch (ExecutionException | InterruptedException e) { + e.printStackTrace(); + } + } + } + } + + //--- URLs ---- + Matcher matcherALink = Patterns.WEB_URL.matcher(content); + int offSetTruncate = 0; + while (matcherALink.find()) { + int matchStart = matcherALink.start() - offSetTruncate; + int matchEnd = matchStart + matcherALink.group().length(); + //Get real URL + /*if (matcherALink.start(1) > matcherALink.end(1) || matcherALink.end() > content.length()) { + continue; + }*/ + final String url = content.toString().substring(matchStart, matchEnd); + //Truncate URL if needed + //TODO: add an option to disable truncated URLs + String urlText = url; + if (url.length() > 30) { + urlText = urlText.substring(0, 30); + urlText += "…"; + content.replace(matchStart, matchEnd, urlText); + matchEnd = matchStart + 31; + offSetTruncate += (url.length() - urlText.length()); + } + if (!urlText.startsWith("http")) { + continue; + } + if (matchStart >= 0 && matchEnd <= content.length() && matchEnd >= matchStart) { + content.setSpan(new ClickableSpan() { + @Override + public void onClick(@NonNull View textView) { + textView.setTag(CLICKABLE_SPAN); + Helper.openBrowser(context, url); + } + + @Override + public void updateDrawState(@NonNull TextPaint ds) { + super.updateDrawState(ds); + ds.setUnderlineText(false); + ds.setColor(linkColor); + } + }, matchStart, matchEnd, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + } + } + + // --- For all patterns defined in Helper class --- + for (Map.Entry entry : Helper.patternHashMap.entrySet()) { + Helper.PatternType patternType = entry.getKey(); + Pattern pattern = entry.getValue(); + Matcher matcher = pattern.matcher(content); + while (matcher.find()) { + int matchStart = matcher.start(); + int matchEnd = matcher.end(); + String word = content.toString().substring(matchStart, matchEnd); + if (matchStart >= 0 && matchEnd <= content.toString().length() && matchEnd >= matchStart) { + URLSpan[] span = content.getSpans(matchStart, matchEnd, URLSpan.class); + content.removeSpan(span); + + content.setSpan(new ClickableSpan() { + @Override + public void onClick(@NonNull View textView) { + textView.setTag(CLICKABLE_SPAN); + switch (patternType) { + case TAG: + Intent intent = new Intent(context, HashTagActivity.class); + Bundle b = new Bundle(); + b.putString(Helper.ARG_SEARCH_KEYWORD, word.trim()); + intent.putExtras(b); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + break; + case GROUP: + break; + case MENTION: + intent = new Intent(context, ProfileActivity.class); + b = new Bundle(); + Mention targetedMention = null; + for (Mention mention : status.mentions) { + if (word.trim().compareToIgnoreCase("@" + mention.acct) == 0) { + targetedMention = mention; + break; + } + } + if (targetedMention != null) { + b.putString(Helper.ARG_USER_ID, targetedMention.id); + } else { + b.putString(Helper.ARG_MENTION, word.trim()); + } + intent.putExtras(b); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + break; + case MENTION_LONG: + intent = new Intent(context, ProfileActivity.class); + b = new Bundle(); + targetedMention = null; + for (Mention mention : status.mentions) { + if (word.trim().substring(1).compareToIgnoreCase("@" + mention.acct) == 0) { + targetedMention = mention; + break; + } + } + if (targetedMention != null) { + b.putString(Helper.ARG_USER_ID, targetedMention.id); + } else { + b.putString(Helper.ARG_MENTION, word.trim()); + } + intent.putExtras(b); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + break; + } + } + + @Override + public void updateDrawState(@NonNull TextPaint ds) { + super.updateDrawState(ds); + ds.setUnderlineText(false); + ds.setColor(linkColor); + } + }, matchStart, matchEnd, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + } + } + } + + Matcher matcher = Helper.ouichesPattern.matcher(content); + while (matcher.find()) { + Attachment attachment = new Attachment(); + attachment.type = "audio"; + String tag = matcher.group(1); + attachment.id = tag; + if (tag == null) { + continue; + } + attachment.remote_url = "http://ouich.es/mp3/" + tag + ".mp3"; + attachment.url = "http://ouich.es/mp3/" + tag + ".mp3"; + if (status.media_attachments == null) { + status.media_attachments = new ArrayList<>(); + } + boolean alreadyAdded = false; + for (Attachment at : status.media_attachments) { + if (tag.compareTo(at.id) == 0) { + alreadyAdded = true; + break; + } + } + if (!alreadyAdded) { + status.media_attachments.add(attachment); + } + } + return trimSpannable(new SpannableStringBuilder(content)); + } + + + /** + * Remove extra carriage returns at the bottom due to

tags in toots + * + * @param spannable SpannableStringBuilder + * @return SpannableStringBuilder + */ + private static SpannableStringBuilder trimSpannable(SpannableStringBuilder spannable) { + + int trimStart = 0; + int trimEnd = 0; + String text = spannable.toString(); + + while (text.length() > 0 && text.startsWith("\n")) { + text = text.substring(1); + trimStart += 1; + } + + while (text.length() > 0 && text.endsWith("\n")) { + text = text.substring(0, text.length() - 1); + trimEnd += 1; + } + return spannable.delete(0, trimStart).delete(spannable.length() - trimEnd, spannable.length()); + } + + public static List convertStatus(Context context, List statuses) { + if (statuses != null) { + for (Status status : statuses) { + convertStatus(context, status); + } + } + return statuses; + } + + public static Status convertStatus(Context context, Status status) { + if (status != null) { + status.span_content = SpannableHelper.convert(context, status, status.content); + status.span_spoiler_text = SpannableHelper.convert(context, status, status.spoiler_text); + if (status.translationContent != null) { + status.span_translate = SpannableHelper.convert(context, status, status.translationContent); + } + status.account.span_display_name = SpannableHelper.convertA(context, status.account, status.account.display_name, true); + if (status.poll != null) { + for (Poll.PollItem pollItem : status.poll.options) { + pollItem.span_title = SpannableHelper.convert(context, status, pollItem.title); + } + } + if (status.reblog != null) { + status.reblog.span_content = SpannableHelper.convert(context, status, status.reblog.content); + if (status.reblog.translationContent != null) { + status.reblog.span_translate = SpannableHelper.convert(context, status, status.reblog.translationContent); + } + status.reblog.span_spoiler_text = SpannableHelper.convert(context, status, status.reblog.spoiler_text); + status.reblog.account.span_display_name = SpannableHelper.convertA(context, status.reblog.account, status.reblog.account.display_name, true); + if (status.reblog.poll != null) { + for (Poll.PollItem pollItem : status.reblog.poll.options) { + pollItem.span_title = SpannableHelper.convert(context, status, pollItem.title); + } + } + } + } + return status; + } + + + public static List convertAccounts(Context context, List accounts) { + if (accounts != null) { + for (Account account : accounts) { + convertAccount(context, account); + } + } + return accounts; + } + + public static Account convertAccount(Context context, Account account) { + if (account != null) { + account.span_display_name = SpannableHelper.convertA(context, account, account.display_name, true); + account.span_note = SpannableHelper.convertA(context, account, account.note, false); + if (account.fields != null && account.fields.size() > 0) { + List fields = new ArrayList<>(); + for (Field field : account.fields) { + field.value_span = SpannableHelper.convertA(context, account, field.value, false); + fields.add(field); + } + account.fields = fields; + } + } + return account; + } + + + /** + * Convert HTML content to text. Also, it handles click on link and transform emoji + * This needs to be run asynchronously + * + * @param context {@link Context} + * @param account {@link Account} - Account concerned by the spannable transformation + * @param text String - text to convert, it can be display name or bio + * @return Spannable string + */ + private static Spannable convertA(@NonNull Context context, @NonNull Account account, @NonNull String text, boolean limitedToDisplayName) { + SpannableString initialContent; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + initialContent = new SpannableString(Html.fromHtml(text, Html.FROM_HTML_MODE_LEGACY)); + else + initialContent = new SpannableString(Html.fromHtml(text)); + + SpannableStringBuilder content = new SpannableStringBuilder(initialContent); + URLSpan[] urls = content.getSpans(0, (content.length() - 1), URLSpan.class); + for (URLSpan span : urls) + content.removeSpan(span); + //--- EMOJI ---- + SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(context); + boolean disableGif = sharedpreferences.getBoolean(context.getString(R.string.SET_DISABLE_GIF), false); + //Will convert emoji if asked + if (account.emojis != null && account.emojis.size() > 0) { + for (Emoji emoji : account.emojis) { + if (Helper.isValidContextForGlide(context)) { + FutureTarget futureTarget = Glide.with(context) + .asFile() + .load(disableGif ? emoji.static_url : emoji.url) + .submit(); + try { + File file = futureTarget.get(); + final String targetedEmoji = ":" + emoji.shortcode + ":"; + if (content.toString().contains(targetedEmoji)) { + //emojis can be used several times so we have to loop + for (int startPosition = -1; (startPosition = content.toString().indexOf(targetedEmoji, startPosition + 1)) != -1; startPosition++) { + final int endPosition = startPosition + targetedEmoji.length(); + if (endPosition <= content.toString().length() && endPosition >= startPosition) { + ImageSpan imageSpan; + if (APNGParser.isAPNG(file.getAbsolutePath())) { + APNGDrawable apngDrawable = APNGDrawable.fromFile(file.getAbsolutePath()); + try { + apngDrawable.setBounds(0, 0, (int) convertDpToPixel(20, context), (int) convertDpToPixel(20, context)); + apngDrawable.setVisible(true, true); + imageSpan = new ImageSpan(apngDrawable); + content.setSpan( + imageSpan, startPosition, + endPosition, Spannable.SPAN_INCLUSIVE_EXCLUSIVE); + + } catch (Exception ignored) { + } + } else if (GifParser.isGif(file.getAbsolutePath())) { + GifDrawable gifDrawable = GifDrawable.fromFile(file.getAbsolutePath()); + try { + gifDrawable.setBounds(0, 0, (int) convertDpToPixel(20, context), (int) convertDpToPixel(20, context)); + gifDrawable.setVisible(true, true); + imageSpan = new ImageSpan(gifDrawable); + content.setSpan( + imageSpan, startPosition, + endPosition, Spannable.SPAN_INCLUSIVE_EXCLUSIVE); + } catch (Exception ignored) { + } + } else { + Drawable drawable = Drawable.createFromPath(file.getAbsolutePath()); + try { + drawable.setBounds(0, 0, (int) convertDpToPixel(20, context), (int) convertDpToPixel(20, context)); + drawable.setVisible(true, true); + imageSpan = new ImageSpan(drawable); + content.setSpan( + imageSpan, startPosition, + endPosition, Spannable.SPAN_INCLUSIVE_EXCLUSIVE); + } catch (Exception ignored) { + } + } + } + } + } + } catch (ExecutionException | InterruptedException e) { + e.printStackTrace(); + } + } + } + } + if (limitedToDisplayName) { + return content; + } + //--- URLs ---- + Matcher matcherALink = Patterns.WEB_URL.matcher(content); + + int offSetTruncate = 0; + while (matcherALink.find()) { + int matchStart = matcherALink.start() - offSetTruncate; + int matchEnd = matchStart + matcherALink.group().length(); + //Get real URL + if (matcherALink.start(1) > matcherALink.end(1) || matcherALink.end() > content.length()) { + continue; + } + final String url = content.toString().substring(matchStart, matchEnd); + //Truncate URL if needed + //TODO: add an option to disable truncated URLs + String urlText = url; + if (url.length() > 30 && matchStart < matchEnd) { + urlText = urlText.substring(0, 30); + urlText += "…"; + content.replace(matchStart, matchEnd, urlText); + matchEnd = matcherALink.end() - (url.length() - urlText.length()); + offSetTruncate += (url.length() - urlText.length()); + } + if (!urlText.startsWith("http")) { + continue; + } + if (matchStart >= 0 && matchEnd <= content.toString().length() && matchEnd >= matchStart) + content.setSpan(new ClickableSpan() { + @Override + public void onClick(@NonNull View textView) { + textView.setTag(CLICKABLE_SPAN); + Helper.openBrowser(context, url); + } + + @Override + public void updateDrawState(@NonNull TextPaint ds) { + super.updateDrawState(ds); + ds.setUnderlineText(false); + ds.setColor(linkColor); + } + }, matchStart, matchEnd, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + } + + // --- For all patterns defined in Helper class --- + for (Map.Entry entry : Helper.patternHashMap.entrySet()) { + Helper.PatternType patternType = entry.getKey(); + Pattern pattern = entry.getValue(); + Matcher matcher = pattern.matcher(content); + while (matcher.find()) { + int matchStart = matcher.start(); + int matchEnd = matcher.end(); + if (matchStart >= 0 && matchEnd <= content.toString().length() && matchEnd >= matchStart) { + URLSpan[] span = content.getSpans(matchStart, matchEnd, URLSpan.class); + content.removeSpan(span); + String word = content.toString().substring(matchStart, matchEnd); + content.setSpan(new ClickableSpan() { + @Override + public void onClick(@NonNull View textView) { + textView.setTag(CLICKABLE_SPAN); + switch (patternType) { + case TAG: + Intent intent = new Intent(context, HashTagActivity.class); + Bundle b = new Bundle(); + b.putString(Helper.ARG_SEARCH_KEYWORD, word.trim()); + intent.putExtras(b); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + break; + case GROUP: + break; + case MENTION_LONG: + case MENTION: + intent = new Intent(context, ProfileActivity.class); + b = new Bundle(); + b.putString(Helper.ARG_MENTION, word.trim()); + intent.putExtras(b); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + break; + } + } + + @Override + public void updateDrawState(@NonNull TextPaint ds) { + super.updateDrawState(ds); + ds.setUnderlineText(false); + ds.setColor(linkColor); + } + }, matchStart, matchEnd, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + } + } + } + return trimSpannable(new SpannableStringBuilder(content)); + } + + /** + * Makes the move to account clickable + * + * @param context Context + * @return SpannableString + */ + public static SpannableString moveToText(final Context context, Account account) { + SpannableString spannableString = null; + if (account.moved != null) { + spannableString = new SpannableString(context.getString(R.string.account_moved_to, account.acct, "@" + account.moved.acct)); + int startPosition = spannableString.toString().indexOf("@" + account.moved.acct); + int endPosition = startPosition + ("@" + account.moved.acct).length(); + if (startPosition >= 0 && endPosition <= spannableString.toString().length() && endPosition >= startPosition) + spannableString.setSpan(new ClickableSpan() { + @Override + public void onClick(@NonNull View textView) { + Intent intent = new Intent(context, ProfileActivity.class); + Bundle b = new Bundle(); + b.putSerializable(Helper.ARG_ACCOUNT, account.moved); + intent.putExtras(b); + context.startActivity(intent); + } + + @Override + public void updateDrawState(@NonNull TextPaint ds) { + super.updateDrawState(ds); + } + }, + startPosition, endPosition, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + } + return spannableString; + } +} diff --git a/app/src/main/java/app/fedilab/android/helper/ThemeHelper.java b/app/src/main/java/app/fedilab/android/helper/ThemeHelper.java new file mode 100644 index 000000000..e3843c1eb --- /dev/null +++ b/app/src/main/java/app/fedilab/android/helper/ThemeHelper.java @@ -0,0 +1,375 @@ +package app.fedilab.android.helper; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.res.ColorStateList; +import android.content.res.Resources; +import android.os.Bundle; +import android.util.TypedValue; +import android.view.View; +import android.view.animation.Animation; +import android.view.animation.TranslateAnimation; + +import androidx.annotation.AttrRes; +import androidx.annotation.ColorInt; +import androidx.core.content.ContextCompat; +import androidx.core.graphics.ColorUtils; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; +import androidx.preference.PreferenceManager; + +import com.google.android.material.button.MaterialButton; +import com.jaredrummler.cyanea.Cyanea; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; + +import app.fedilab.android.R; +import app.fedilab.android.activities.BaseActivity; + +public class ThemeHelper { + + public static int linkColor; + + @ColorInt + public static int getAttColor(Context context, @AttrRes int attColor) { + TypedValue typedValue = new TypedValue(); + Resources.Theme theme = context.getTheme(); + theme.resolveAttribute(attColor, typedValue, true); + return typedValue.data; + } + + /** + * Initialize colors in a static variable + * Currently link_color cannot be retrieved with getAttColor in ViewModel due to application and theme + * + * @param activity Activity + */ + public static void initiliazeColors(Activity activity) { + TypedValue typedValue = new TypedValue(); + Resources.Theme theme = activity.getTheme(); + theme.resolveAttribute(R.attr.linkColor, typedValue, true); + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activity); + linkColor = -1; + if (prefs.getBoolean("use_custom_theme", false)) { + linkColor = prefs.getInt("theme_link_color", -1); + } + if (linkColor == -1) { + linkColor = typedValue.data; + } + } + + public static void applyTheme(BaseActivity activity) { + if (Cyanea.getInstance().isDark()) { + activity.setTheme(R.style.AppThemeDark); + } else { + activity.setTheme(R.style.AppTheme); + } + } + + public static void applyThemeDialog(BaseActivity activity) { + if (Cyanea.getInstance().isDark()) { + activity.setTheme(R.style.DialogDark); + } else { + activity.setTheme(R.style.Dialog); + } + } + + public static void applyThemeBar(BaseActivity activity) { + if (Cyanea.getInstance().isDark()) { + activity.setTheme(R.style.AppThemeBarDark); + } else { + activity.setTheme(R.style.AppThemeBar); + } + } + + /** + * Animate two views, the current view will be hidden to left + * + * @param viewToHide View to hide + * @param viewToShow View to show + * @param slideAnimation listener for the animation + */ + public static void slideViewsToLeft(View viewToHide, View viewToShow, SlideAnimation slideAnimation) { + + TranslateAnimation animateHide = new TranslateAnimation( + 0, + -viewToHide.getWidth(), + 0, + 0); + TranslateAnimation animateShow = new TranslateAnimation( + viewToShow.getWidth(), + 0, + 0, + 0); + animateShow.setAnimationListener(new Animation.AnimationListener() { + @Override + public void onAnimationStart(Animation animation) { + viewToShow.setVisibility(View.VISIBLE); + viewToHide.setVisibility(View.VISIBLE); + } + + @Override + public void onAnimationEnd(Animation animation) { + viewToHide.setVisibility(View.GONE); + if (slideAnimation != null) { + slideAnimation.onAnimationEnded(); + } + } + + @Override + public void onAnimationRepeat(Animation animation) { + + } + }); + animateHide.setDuration(300); + animateHide.setFillAfter(true); + animateShow.setDuration(300); + animateShow.setFillAfter(true); + viewToHide.startAnimation(animateHide); + viewToShow.startAnimation(animateShow); + } + + /** + * Animate two views, the current view will be hidden to right + * + * @param viewToHide View to hide + * @param viewToShow View to show + * @param slideAnimation listener for the animation + */ + public static void slideViewsToRight(View viewToHide, View viewToShow, SlideAnimation slideAnimation) { + + TranslateAnimation animateHide = new TranslateAnimation( + 0, + viewToHide.getWidth(), + 0, + 0); + TranslateAnimation animateShow = new TranslateAnimation( + -viewToShow.getWidth(), + 0, + 0, + 0); + animateShow.setAnimationListener(new Animation.AnimationListener() { + @Override + public void onAnimationStart(Animation animation) { + viewToShow.setVisibility(View.VISIBLE); + viewToHide.setVisibility(View.VISIBLE); + if (slideAnimation != null) { + slideAnimation.onAnimationEnded(); + } + } + + @Override + public void onAnimationEnd(Animation animation) { + viewToHide.setVisibility(View.GONE); + } + + @Override + public void onAnimationRepeat(Animation animation) { + + } + }); + animateHide.setDuration(300); + animateShow.setDuration(300); + viewToHide.requestLayout(); + viewToHide.startAnimation(animateHide); + viewToShow.startAnimation(animateShow); + } + + public interface SlideAnimation { + void onAnimationEnded(); + } + + + public static List> getContributorsTheme(Context context) { + List> linkedHashMaps = new ArrayList<>(); + String[] list; + try { + list = context.getAssets().list("themes/contributors"); + if (list.length > 0) { + for (String file : list) { + InputStream is = context.getAssets().open("themes/contributors/" + file); + LinkedHashMap data = readCSVFile(is); + linkedHashMaps.add(data); + } + } + } catch (IOException e) { + e.printStackTrace(); + } + return linkedHashMaps; + } + + + private static LinkedHashMap readCSVFile(InputStream inputStream) { + LinkedHashMap readValues = new LinkedHashMap<>(); + if (inputStream != null) { + BufferedReader br = null; + try { + br = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)); + String sCurrentLine; + while ((sCurrentLine = br.readLine()) != null) { + String[] line = sCurrentLine.split(","); + if (line.length > 1) { + String key = line[0]; + String value = line[1]; + readValues.put(key, value); + } + } + } catch (IOException e) { + e.printStackTrace(); + } finally { + try { + if (br != null) br.close(); + } catch (IOException ex) { + ex.printStackTrace(); + } + } + } + return readValues; + } + + + /** + * Allow to set colors for tablayout + * + * @param context - Context + * @return - ColorStateList + */ + public static ColorStateList getColorStateList(Context context) { + int[][] states = new int[][]{ + new int[]{android.R.attr.state_selected}, + new int[]{-android.R.attr.state_selected}, + }; + int[] colors = new int[]{ + ContextCompat.getColor(context, R.color.cyanea_accent_dark_reference), + getAttColor(context, R.attr.mTextColor) + }; + return new ColorStateList(states, colors); + } + + /** + * Change all color for a Material button + * + * @param context - Context + * @param materialButton - MaterialButton + */ + public static void changeButtonColor(Context context, MaterialButton materialButton) { + materialButton.setRippleColor(ThemeHelper.getButtonColorStateList(context)); + materialButton.setStrokeColor(ThemeHelper.getButtonColorStateList(context)); + materialButton.setTextColor(ThemeHelper.getButtonColorStateList(context)); + materialButton.setIconTint(ThemeHelper.getButtonColorStateList(context)); + materialButton.setBackgroundTintList(ThemeHelper.getBackgroundButtonColorStateList(context)); + } + + + /** + * Allow to set ThumbDrawable colors for SwitchCompat + * + * @param context - Context + * @return - ColorStateList + */ + public static ColorStateList getSwitchCompatThumbDrawable(Context context) { + int[][] states = new int[][]{ + new int[]{android.R.attr.state_checked}, + new int[]{-android.R.attr.state_checked}, + }; + int alphaColor = ColorUtils.setAlphaComponent(ContextCompat.getColor(context, R.color.cyanea_accent_dark_reference), 0xee); + int alphaColorUnchecked = ColorUtils.setAlphaComponent(getAttColor(context, R.attr.mTextColor), 0xaa); + int[] colors = new int[]{ + alphaColor, + alphaColorUnchecked + }; + return new ColorStateList(states, colors); + } + + /** + * Allow to set TrackDrawable colors for SwitchCompat + * + * @param context - Context + * @return - ColorStateList + */ + public static ColorStateList getSwitchCompatTrackDrawable(Context context) { + int[][] states = new int[][]{ + new int[]{android.R.attr.state_checked}, + new int[]{-android.R.attr.state_checked}, + }; + int alphaColor = ColorUtils.setAlphaComponent(ContextCompat.getColor(context, R.color.cyanea_accent_dark_reference), 0x33); + int alphaColorUnchecked = ColorUtils.setAlphaComponent(getAttColor(context, R.attr.mTextColor), 0x33); + int[] colors = new int[]{ + alphaColor, + alphaColorUnchecked + }; + return new ColorStateList(states, colors); + } + + /** + * Allow to set colors for Material buttons inside a toggle group + * + * @param context - Context + * @return - ColorStateList + */ + public static ColorStateList getButtonColorStateList(Context context) { + int[][] states = new int[][]{ + new int[]{android.R.attr.state_checked}, + new int[]{-android.R.attr.state_checked}, + }; + int[] colors = new int[]{ + ContextCompat.getColor(context, R.color.cyanea_accent_dark_reference), + getAttColor(context, R.attr.mTextColor) + }; + return new ColorStateList(states, colors); + } + + /** + * Allow to set background colors for Material buttons inside a toggle group + * + * @param context - Context + * @return - ColorStateList + */ + private static ColorStateList getBackgroundButtonColorStateList(Context context) { + int[][] states = new int[][]{ + new int[]{android.R.attr.state_checked}, + new int[]{-android.R.attr.state_checked}, + }; + int alphaColor = ColorUtils.setAlphaComponent(ContextCompat.getColor(context, R.color.cyanea_accent_dark_reference), 0x33); + int[] colors = new int[]{ + alphaColor, + ContextCompat.getColor(context, R.color.transparent) + }; + return new ColorStateList(states, colors); + } + + /** + * Send broadcast to recreate Mainactivity + * + * @param activity - Activity + */ + public static void recreateMainActivity(Activity activity) { + Bundle b = new Bundle(); + b.putBoolean(Helper.RECEIVE_RECREATE_ACTIVITY, true); + Intent intentBD = new Intent(Helper.BROADCAST_DATA); + intentBD.putExtras(b); + LocalBroadcastManager.getInstance(activity).sendBroadcast(intentBD); + } +} diff --git a/app/src/main/java/app/fedilab/android/helper/TimelineHelper.java b/app/src/main/java/app/fedilab/android/helper/TimelineHelper.java new file mode 100644 index 000000000..7e8a73bf2 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/helper/TimelineHelper.java @@ -0,0 +1,178 @@ +package app.fedilab.android.helper; +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import android.content.Context; +import android.os.Build; +import android.text.Html; + +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.ViewModelProvider; +import androidx.lifecycle.ViewModelStoreOwner; + +import com.google.gson.annotations.SerializedName; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import app.fedilab.android.BaseMainActivity; +import app.fedilab.android.client.mastodon.entities.Filter; +import app.fedilab.android.client.mastodon.entities.Notification; +import app.fedilab.android.client.mastodon.entities.Status; +import app.fedilab.android.viewmodel.mastodon.AccountsVM; + +public class TimelineHelper { + + /** + * Allows to filter statuses, should be called in API calls (background) + * + * @param context - Context + * @param statuses - List of {@link Status} + * @param filterTimeLineType - {@link FilterTimeLineType} + * @return filtered List + */ + public static List filterStatus(Context context, List statuses, FilterTimeLineType filterTimeLineType) { + //A security to make sure filters have been fetched before displaying messages + List statusesToRemove = new ArrayList<>(); + if (!BaseMainActivity.filterFetched) { + AccountsVM accountsVM = new ViewModelProvider((ViewModelStoreOwner) context).get(AccountsVM.class); + accountsVM.getFilters(BaseMainActivity.currentInstance, BaseMainActivity.currentToken).observe((LifecycleOwner) context, filters -> { + BaseMainActivity.filterFetched = true; + BaseMainActivity.mainFilters = filters; + }); + } + //If there are filters: + if (BaseMainActivity.mainFilters != null && BaseMainActivity.mainFilters.size() > 0) { + for (Filter filter : BaseMainActivity.mainFilters) { + if (filter.irreversible) { //Dealt by the server + continue; + } + for (String filterContext : filter.context) { + if (filterTimeLineType.value.equalsIgnoreCase(filterContext)) { + if (filter.whole_word) { + Pattern p = Pattern.compile("(^" + Pattern.quote(filter.phrase) + "\\b|\\b" + Pattern.quote(filter.phrase) + "$)"); + for (Status status : statuses) { + String content; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + content = Html.fromHtml(status.content, Html.FROM_HTML_MODE_LEGACY).toString(); + else + content = Html.fromHtml(status.content).toString(); + Matcher m = p.matcher(content); + if (m.find()) { + statusesToRemove.add(status); + } + } + } else { + for (Status status : statuses) { + String content; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + content = Html.fromHtml(status.content, Html.FROM_HTML_MODE_LEGACY).toString(); + else + content = Html.fromHtml(status.content).toString(); + if (content.contains(filter.phrase)) { + statusesToRemove.add(status); + } + } + } + } + } + } + } + statuses.removeAll(statusesToRemove); + return statuses; + } + + /** + * Allows to filter notifications, should be called in API calls (background) + * + * @param context - Context + * @param notifications - List of {@link Notification} + * @return filtered List + */ + public static List filterNotification(Context context, List notifications) { + //A security to make sure filters have been fetched before displaying messages + List notificationToRemove = new ArrayList<>(); + if (!BaseMainActivity.filterFetched) { + AccountsVM accountsVM = new ViewModelProvider((ViewModelStoreOwner) context).get(AccountsVM.class); + accountsVM.getFilters(BaseMainActivity.currentInstance, BaseMainActivity.currentToken).observe((LifecycleOwner) context, filters -> { + BaseMainActivity.filterFetched = true; + BaseMainActivity.mainFilters = filters; + }); + } + //If there are filters: + if (BaseMainActivity.mainFilters != null && BaseMainActivity.mainFilters.size() > 0) { + for (Filter filter : BaseMainActivity.mainFilters) { + if (filter.irreversible) { //Dealt by the server + continue; + } + for (String filterContext : filter.context) { + if (FilterTimeLineType.NOTIFICATION.value.equalsIgnoreCase(filterContext)) { + if (filter.whole_word) { + Pattern p = Pattern.compile("(^" + Pattern.quote(filter.phrase) + "\\b|\\b" + Pattern.quote(filter.phrase) + "$)"); + for (Notification notification : notifications) { + if (notification.status != null) { + String content; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + content = Html.fromHtml(notification.status.content, Html.FROM_HTML_MODE_LEGACY).toString(); + else + content = Html.fromHtml(notification.status.content).toString(); + Matcher m = p.matcher(content); + if (m.find()) { + notificationToRemove.add(notification); + } + } + } + } else { + for (Notification notification : notifications) { + String content; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + content = Html.fromHtml(notification.status.content, Html.FROM_HTML_MODE_LEGACY).toString(); + else + content = Html.fromHtml(notification.status.content).toString(); + if (content.contains(filter.phrase)) { + notificationToRemove.add(notification); + } + } + } + } + } + } + } + notifications.removeAll(notificationToRemove); + return notifications; + } + + public enum FilterTimeLineType { + @SerializedName("HOME") + HOME("HOME"), + @SerializedName("PUBLIC") + PUBLIC("PUBLIC"), + @SerializedName("CONTEXT") + CONTEXT("CONTEXT"), + @SerializedName("NOTIFICATION") + NOTIFICATION("NOTIFICATION"); + private final String value; + + FilterTimeLineType(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + } +} diff --git a/app/src/main/java/app/fedilab/android/helper/customsharing/CustomSharing.java b/app/src/main/java/app/fedilab/android/helper/customsharing/CustomSharing.java new file mode 100644 index 000000000..e7b0534ec --- /dev/null +++ b/app/src/main/java/app/fedilab/android/helper/customsharing/CustomSharing.java @@ -0,0 +1,113 @@ +package app.fedilab.android.helper.customsharing; +/* Copyright 2019 Curtis Rock + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import android.content.Context; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +import app.fedilab.android.client.mastodon.entities.Error; +import app.fedilab.android.helper.Helper; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; + + +/** + * Created by Curtis on 13/02/2019. + * Manage custom sharing of status metadata to remote content aggregator + */ + +public class CustomSharing { + + private final Context context; + private CustomSharingResponse customSharingResponse; + private Error customSharingError; + + public CustomSharing(Context context) { + this.context = context; + if (context == null) { + customSharingError = new Error(); + return; + } + customSharingResponse = new CustomSharingResponse(); + customSharingError = null; + } + + /*** + * pass status metadata to remote content aggregator *synchronously* + * @return CustomSharingResponse + */ + public CustomSharingResponse customShare(String encodedCustomSharingURL) { + OkHttpClient client = new OkHttpClient.Builder() + .connectTimeout(10, TimeUnit.SECONDS) + .writeTimeout(10, TimeUnit.SECONDS) + .proxy(Helper.getProxy(context.getApplicationContext())) + .readTimeout(10, TimeUnit.SECONDS).build(); + Request request = new Request.Builder() + .url(encodedCustomSharingURL) + .build(); + String HTTPResponse = ""; + try { + Response response = client.newCall(request).execute(); + if (!response.isSuccessful()) { + if (response.body() != null) { + HTTPResponse = response.body().string(); + } + } else { + setError(response.code(), new Exception(response.message())); + } + } catch (IOException e) { + e.printStackTrace(); + setError(500, e); + } + customSharingResponse.setResponse(HTTPResponse); + return customSharingResponse; + } + + public Error getError() { + return customSharingError; + } + + + /** + * Set the error message + * + * @param statusCode int code + * @param error Throwable error + */ + private void setError(int statusCode, Throwable error) { + customSharingError = new Error(); + customSharingError.code = statusCode; + String message = statusCode + " - " + error.getMessage(); + try { + JSONObject jsonObject = new JSONObject(Objects.requireNonNull(error.getMessage())); + String errorM = jsonObject.get("error").toString(); + message = "Error " + statusCode + " : " + errorM; + } catch (JSONException e) { + if (error.getMessage().split("\\.").length > 0) { + String errorM = error.getMessage().split("\\.")[0]; + message = "Error " + statusCode + " : " + errorM; + } + } + customSharingError.error = message; + customSharingResponse.setError(customSharingError); + } +} diff --git a/app/src/main/java/app/fedilab/android/helper/customsharing/CustomSharingAsyncTask.java b/app/src/main/java/app/fedilab/android/helper/customsharing/CustomSharingAsyncTask.java new file mode 100644 index 000000000..dffc5de87 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/helper/customsharing/CustomSharingAsyncTask.java @@ -0,0 +1,52 @@ +package app.fedilab.android.helper.customsharing; +/* Copyright 2017 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import android.content.Context; +import android.os.Handler; +import android.os.Looper; + +import java.lang.ref.WeakReference; + + +/** + * Created by Curtis on 13/02/2019. + * Custom share status metadata to remote content aggregator + */ + +public class CustomSharingAsyncTask { + + private final String encodedCustomSharingURL; + private final OnCustomSharingInterface listener; + private final WeakReference contextReference; + private CustomSharingResponse customSharingResponse; + + public CustomSharingAsyncTask(Context context, String encodedCustomSharingURL, OnCustomSharingInterface onCustomSharingInterface) { + this.contextReference = new WeakReference<>(context); + this.encodedCustomSharingURL = encodedCustomSharingURL; + this.listener = onCustomSharingInterface; + doInBackground(); + } + + protected void doInBackground() { + new Thread(() -> { + customSharingResponse = new CustomSharing(this.contextReference.get()).customShare(encodedCustomSharingURL); + Handler mainHandler = new Handler(Looper.getMainLooper()); + Runnable myRunnable = () -> listener.onCustomSharing(customSharingResponse); + mainHandler.post(myRunnable); + }).start(); + } + +} \ No newline at end of file diff --git a/app/src/main/java/app/fedilab/android/helper/customsharing/CustomSharingResponse.java b/app/src/main/java/app/fedilab/android/helper/customsharing/CustomSharingResponse.java new file mode 100644 index 000000000..eebd1c7e0 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/helper/customsharing/CustomSharingResponse.java @@ -0,0 +1,44 @@ +package app.fedilab.android.helper.customsharing; +/* Copyright 2019 Curtis Rock + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import app.fedilab.android.client.mastodon.entities.Error; + +/** + * Created by Curtis on 13/02/2019. + * Hydrate response from the remote content aggregator for Custom Sharing + */ + +public class CustomSharingResponse { + + private Error error = null; + private String response; + + public Error getError() { + return error; + } + + public void setError(Error error) { + this.error = error; + } + + public String getResponse() { + return response; + } + + public void setResponse(String response) { + this.response = response; + } +} diff --git a/app/src/main/java/app/fedilab/android/helper/customsharing/OnCustomSharingInterface.java b/app/src/main/java/app/fedilab/android/helper/customsharing/OnCustomSharingInterface.java new file mode 100644 index 000000000..d8d358fb6 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/helper/customsharing/OnCustomSharingInterface.java @@ -0,0 +1,24 @@ +/* Copyright 2017 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ +package app.fedilab.android.helper.customsharing; + + +/** + * Created by Curtis on 13/02/2019. + * Interface when custom sharing to remote content aggregator occurs + */ +public interface OnCustomSharingInterface { + void onCustomSharing(CustomSharingResponse customSharingResponse); +} diff --git a/app/src/main/java/app/fedilab/android/helper/itemtouchhelper/ItemTouchHelperAdapter.java b/app/src/main/java/app/fedilab/android/helper/itemtouchhelper/ItemTouchHelperAdapter.java new file mode 100644 index 000000000..4c3517fc0 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/helper/itemtouchhelper/ItemTouchHelperAdapter.java @@ -0,0 +1,57 @@ +package app.fedilab.android.helper.itemtouchhelper; + +/* + * Copyright (C) 2015 Paul Burke + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +import androidx.recyclerview.widget.ItemTouchHelper; +import androidx.recyclerview.widget.RecyclerView; + +/** + * Interface to listen for a move or dismissal event from a {@link ItemTouchHelper.Callback}. + * + * @author Paul Burke (ipaulpro) + */ +public interface ItemTouchHelperAdapter { + + /** + * Called when an item has been dragged far enough to trigger a move. This is called every time + * an item is shifted, and not at the end of a "drop" event.
+ *
+ * Implementations should call {@link RecyclerView.Adapter#notifyItemMoved(int, int)} after + * adjusting the underlying data to reflect this move. + * + * @param fromPosition The start position of the moved item. + * @param toPosition Then resolved position of the moved item. + * @return True if the item was moved to the new adapter position. + * @see RecyclerView#getAdapterPositionFor(RecyclerView.ViewHolder) + * @see RecyclerView.ViewHolder#getAdapterPosition() + */ + boolean onItemMove(int fromPosition, int toPosition); + + + /** + * Called when an item has been dismissed by a swipe.
+ *
+ * Implementations should call {@link RecyclerView.Adapter#notifyItemRemoved(int)} after + * adjusting the underlying data to reflect this removal. + * + * @param position The position of the item dismissed. + * @see RecyclerView#getAdapterPositionFor(RecyclerView.ViewHolder) + * @see RecyclerView.ViewHolder#getAdapterPosition() + */ + void onItemDismiss(int position); +} \ No newline at end of file diff --git a/app/src/main/java/app/fedilab/android/helper/itemtouchhelper/ItemTouchHelperViewHolder.java b/app/src/main/java/app/fedilab/android/helper/itemtouchhelper/ItemTouchHelperViewHolder.java new file mode 100644 index 000000000..a37c15248 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/helper/itemtouchhelper/ItemTouchHelperViewHolder.java @@ -0,0 +1,42 @@ +package app.fedilab.android.helper.itemtouchhelper; + +/* + * Copyright (C) 2015 Paul Burke + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +import androidx.recyclerview.widget.ItemTouchHelper; + +/** + * Interface to notify an item ViewHolder of relevant callbacks from {@link + * ItemTouchHelper.Callback}. + * + * @author Paul Burke (ipaulpro) + */ +public interface ItemTouchHelperViewHolder { + + /** + * Called when the {@link ItemTouchHelper} first registers an item as being moved or swiped. + * Implementations should update the item view to indicate it's active state. + */ + void onItemSelected(); + + + /** + * Called when the {@link ItemTouchHelper} has completed the move or swipe, and the active item + * state should be cleared. + */ + void onItemClear(); +} \ No newline at end of file diff --git a/app/src/main/java/app/fedilab/android/helper/itemtouchhelper/OnStartDragListener.java b/app/src/main/java/app/fedilab/android/helper/itemtouchhelper/OnStartDragListener.java new file mode 100644 index 000000000..68426890e --- /dev/null +++ b/app/src/main/java/app/fedilab/android/helper/itemtouchhelper/OnStartDragListener.java @@ -0,0 +1,34 @@ +package app.fedilab.android.helper.itemtouchhelper; + +/* + * Copyright (C) 2015 Paul Burke + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +import androidx.recyclerview.widget.RecyclerView; + +/** + * Listener for manual initiation of a drag. + */ +public interface OnStartDragListener { + + /** + * Called when a view is requesting a start of a drag. + * + * @param viewHolder The holder of the view to drag. + */ + void onStartDrag(RecyclerView.ViewHolder viewHolder); + +} diff --git a/app/src/main/java/app/fedilab/android/helper/itemtouchhelper/OnUndoListener.java b/app/src/main/java/app/fedilab/android/helper/itemtouchhelper/OnUndoListener.java new file mode 100644 index 000000000..6ba1a0831 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/helper/itemtouchhelper/OnUndoListener.java @@ -0,0 +1,34 @@ +package app.fedilab.android.helper.itemtouchhelper; + +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + + +import app.fedilab.android.client.entities.app.PinnedTimeline; + +/** + * Listener for manual initiation of a undo. + */ +public interface OnUndoListener { + + /** + * Called when an item is removed + * + * @param pinnedTimeline The timeline PinnedTimeline + * @param position The position of the item in tablayout + */ + void onUndo(PinnedTimeline pinnedTimeline, int position); + +} diff --git a/app/src/main/java/app/fedilab/android/helper/itemtouchhelper/SimpleItemTouchHelperCallback.java b/app/src/main/java/app/fedilab/android/helper/itemtouchhelper/SimpleItemTouchHelperCallback.java new file mode 100644 index 000000000..f929d9d02 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/helper/itemtouchhelper/SimpleItemTouchHelperCallback.java @@ -0,0 +1,127 @@ +package app.fedilab.android.helper.itemtouchhelper; + +/* + * Copyright (C) 2015 Paul Burke + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +import android.graphics.Canvas; + +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.ItemTouchHelper; +import androidx.recyclerview.widget.RecyclerView; + +import org.jetbrains.annotations.NotNull; + +/** + * An implementation of {@link ItemTouchHelper.Callback} that enables basic drag & drop and + * swipe-to-dismiss. Drag events are automatically started by an item long-press.
+ *
+ * Expects the RecyclerView.Adapter to listen for {@link + * ItemTouchHelperAdapter} callbacks and the RecyclerView.ViewHolder to implement + * {@link ItemTouchHelperViewHolder}. + * + * @author Paul Burke (ipaulpro) + */ +public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback { + + private static final float ALPHA_FULL = 1.0f; + + private final ItemTouchHelperAdapter mAdapter; + + public SimpleItemTouchHelperCallback(ItemTouchHelperAdapter adapter) { + mAdapter = adapter; + } + + @Override + public boolean isLongPressDragEnabled() { + return true; + } + + @Override + public boolean isItemViewSwipeEnabled() { + return true; + } + + @Override + public int getMovementFlags(@NotNull RecyclerView recyclerView, @NotNull RecyclerView.ViewHolder viewHolder) { + // Set movement flags based on the layout manager + if (recyclerView.getLayoutManager() instanceof GridLayoutManager) { + final int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT; + final int swipeFlags = 0; + return makeMovementFlags(dragFlags, swipeFlags); + } else { + final int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN; + final int swipeFlags = ItemTouchHelper.START | ItemTouchHelper.END; + return makeMovementFlags(dragFlags, swipeFlags); + } + } + + @Override + public boolean onMove(@NotNull RecyclerView recyclerView, @NotNull RecyclerView.ViewHolder source, @NotNull RecyclerView.ViewHolder target) { + if (source.getItemViewType() != target.getItemViewType()) { + return false; + } + + // Notify the adapter of the move + mAdapter.onItemMove(source.getAdapterPosition(), target.getAdapterPosition()); + return true; + } + + @Override + public void onSwiped(@NotNull RecyclerView.ViewHolder viewHolder, int i) { + // Notify the adapter of the dismissal + mAdapter.onItemDismiss(viewHolder.getAdapterPosition()); + } + + @Override + public void onChildDraw(@NotNull Canvas c, @NotNull RecyclerView recyclerView, @NotNull RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) { + if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) { + // Fade out the view as it is swiped out of the parent's bounds + final float alpha = ALPHA_FULL - Math.abs(dX) / (float) viewHolder.itemView.getWidth(); + viewHolder.itemView.setAlpha(alpha); + viewHolder.itemView.setTranslationX(dX); + } else { + super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive); + } + } + + @Override + public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) { + // We only want the active item to change + if (actionState != ItemTouchHelper.ACTION_STATE_IDLE) { + if (viewHolder instanceof ItemTouchHelperViewHolder) { + // Let the view holder know that this item is being moved or dragged + ItemTouchHelperViewHolder itemViewHolder = (ItemTouchHelperViewHolder) viewHolder; + itemViewHolder.onItemSelected(); + } + } + + super.onSelectedChanged(viewHolder, actionState); + } + + @Override + public void clearView(@NotNull RecyclerView recyclerView, @NotNull RecyclerView.ViewHolder viewHolder) { + super.clearView(recyclerView, viewHolder); + + viewHolder.itemView.setAlpha(ALPHA_FULL); + + if (viewHolder instanceof ItemTouchHelperViewHolder) { + // Tell the view holder it's time to restore the idle state + ItemTouchHelperViewHolder itemViewHolder = (ItemTouchHelperViewHolder) viewHolder; + itemViewHolder.onItemClear(); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/fedilab/android/helper/settings/LongSummaryPreferenceCategory.java b/app/src/main/java/app/fedilab/android/helper/settings/LongSummaryPreferenceCategory.java new file mode 100644 index 000000000..56a6b0cbd --- /dev/null +++ b/app/src/main/java/app/fedilab/android/helper/settings/LongSummaryPreferenceCategory.java @@ -0,0 +1,55 @@ +package app.fedilab.android.helper.settings; +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.preference.PreferenceCategory; +import androidx.preference.PreferenceViewHolder; + +public class LongSummaryPreferenceCategory extends PreferenceCategory { + + public LongSummaryPreferenceCategory(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + public LongSummaryPreferenceCategory(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public LongSummaryPreferenceCategory(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public LongSummaryPreferenceCategory(@NonNull Context context) { + super(context); + } + + @Override + public void onBindViewHolder(@NonNull PreferenceViewHolder holder) { + super.onBindViewHolder(holder); + TextView summary = (TextView) holder.findViewById(android.R.id.summary); + if (summary != null) { + summary.setSingleLine(false); + summary.setMaxLines(10); + } + } + + +} diff --git a/app/src/main/java/app/fedilab/android/helper/settings/TimePreference.java b/app/src/main/java/app/fedilab/android/helper/settings/TimePreference.java new file mode 100644 index 000000000..34b1fe78d --- /dev/null +++ b/app/src/main/java/app/fedilab/android/helper/settings/TimePreference.java @@ -0,0 +1,86 @@ +package app.fedilab.android.helper.settings; +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import android.content.Context; +import android.content.res.TypedArray; +import android.util.AttributeSet; + +import androidx.preference.DialogPreference; + +import app.fedilab.android.R; + +public class TimePreference extends DialogPreference { + + private String time; + + public TimePreference(Context context) { + // Delegate to other constructor + this(context, null); + } + + public TimePreference(Context context, AttributeSet attrs) { + this(context, attrs, R.attr.preferenceStyle); + } + + public TimePreference(Context context, AttributeSet attrs, int defStyleAttr) { + // Delegate to other constructor + this(context, attrs, defStyleAttr, defStyleAttr); + } + + public TimePreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + setPositiveButtonText(R.string.validate); + setNegativeButtonText(R.string.cancel); + } + + public static int getHour(String time) { + String[] pieces = time.split(":"); + + return Integer.parseInt(pieces[0]); + } + + public static int getMinute(String time) { + String[] pieces = time.split(":"); + + return Integer.parseInt(pieces[1]); + } + + public String getTime() { + return time; + } + + public void setTime(String time) { + this.time = time; + persistString(time); + } + + @Override + protected Object onGetDefaultValue(TypedArray a, int index) { + return a.getString(index); + } + + @Override + public int getDialogLayoutResource() { + return R.layout.preference_time; + } + + @Override + protected void onSetInitialValue(boolean restorePersistedValue, Object defaultValue) { + setTime(restorePersistedValue ? + getPersistedString(time) : (String) defaultValue); + } +} diff --git a/app/src/main/java/app/fedilab/android/helper/settings/TimePreferenceDialogFragment.java b/app/src/main/java/app/fedilab/android/helper/settings/TimePreferenceDialogFragment.java new file mode 100644 index 000000000..41f0ddb66 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/helper/settings/TimePreferenceDialogFragment.java @@ -0,0 +1,84 @@ +package app.fedilab.android.helper.settings; +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import android.os.Bundle; +import android.text.format.DateFormat; +import android.view.View; +import android.widget.TimePicker; + +import androidx.annotation.NonNull; +import androidx.preference.DialogPreference; +import androidx.preference.PreferenceDialogFragmentCompat; + +import app.fedilab.android.R; + +public class TimePreferenceDialogFragment extends PreferenceDialogFragmentCompat { + + private TimePicker mTimePicker; + + + public static TimePreferenceDialogFragment newInstance(String key) { + final TimePreferenceDialogFragment + fragment = new TimePreferenceDialogFragment(); + final Bundle b = new Bundle(1); + b.putString(ARG_KEY, key); + fragment.setArguments(b); + + return fragment; + } + + @Override + protected void onBindDialogView(@NonNull View view) { + super.onBindDialogView(view); + + mTimePicker = view.findViewById(R.id.time_picker); + + if (mTimePicker == null) { + throw new IllegalStateException("Dialog view must contain a TimePicker with id 'time_picker'"); + } + + String time = null; + DialogPreference preference = getPreference(); + if (preference instanceof TimePreference) { + time = ((TimePreference) preference).getTime(); + } + + // Set the time to the TimePicker + if (time != null) { + mTimePicker.setIs24HourView(DateFormat.is24HourFormat(getContext())); + mTimePicker.setCurrentHour(TimePreference.getHour(time)); + mTimePicker.setCurrentMinute(TimePreference.getMinute(time)); + } + } + + @Override + public void onDialogClosed(boolean positiveResult) { + if (positiveResult) { + // Get the current values from the TimePicker + int hour = mTimePicker.getCurrentHour(); + int minute = mTimePicker.getCurrentMinute(); + // Generate value to save + String time = hour + ":" + minute; + DialogPreference preference = getPreference(); + if (preference instanceof TimePreference) { + TimePreference timePreference = ((TimePreference) preference); + if (timePreference.callChangeListener(time)) { + timePreference.setTime(time); + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/fedilab/android/interfaces/OnDownloadInterface.java b/app/src/main/java/app/fedilab/android/interfaces/OnDownloadInterface.java new file mode 100644 index 000000000..0e4cf717f --- /dev/null +++ b/app/src/main/java/app/fedilab/android/interfaces/OnDownloadInterface.java @@ -0,0 +1,21 @@ +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ +package app.fedilab.android.interfaces; + +public interface OnDownloadInterface { + void onDownloaded(String saveFilePath, String downloadUrl, Error error); + + void onUpdateProgress(int progress); +} diff --git a/app/src/main/java/app/fedilab/android/jobs/NotificationsWorker.java b/app/src/main/java/app/fedilab/android/jobs/NotificationsWorker.java new file mode 100644 index 000000000..b1e502570 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/jobs/NotificationsWorker.java @@ -0,0 +1,98 @@ +package app.fedilab.android.jobs; +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.content.Context; +import android.content.Intent; +import android.graphics.BitmapFactory; +import android.os.Build; + +import androidx.annotation.NonNull; +import androidx.core.app.NotificationCompat; +import androidx.work.Data; +import androidx.work.ForegroundInfo; +import androidx.work.Worker; +import androidx.work.WorkerParameters; + +import app.fedilab.android.R; +import app.fedilab.android.client.entities.StatusDraft; +import app.fedilab.android.exception.DBException; +import app.fedilab.android.helper.Helper; +import app.fedilab.android.services.PostMessageService; + +public class NotificationsWorker extends Worker { + + private static final int FETCH_NOTIFICATION_CHANNEL_ID = 4; + private static final String CHANNEL_ID = "notifications"; + private final NotificationManager notificationManager; + + public NotificationsWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) { + super(context, workerParams); + notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + } + + @NonNull + private ForegroundInfo createForegroundInfo() { + if (Build.VERSION.SDK_INT >= 26) { + String channelName = "Notification"; + String channelDescription = "Fetched notifications"; + NotificationChannel notifChannel = new NotificationChannel(CHANNEL_ID, channelName, NotificationManager.IMPORTANCE_HIGH); + notifChannel.setDescription(channelDescription); + notificationManager.createNotificationChannel(notifChannel); + + } + NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(getApplicationContext(), CHANNEL_ID); + notificationBuilder.setSmallIcon(R.drawable.ic_notification) + .setLargeIcon(BitmapFactory.decodeResource(getApplicationContext().getResources(), R.drawable.ic_launcher_foreground)) + .setContentTitle(getApplicationContext().getString(R.string.scheduled_toots)) + .setContentText(getApplicationContext().getString(R.string.scheduled_toots)) + .setDefaults(NotificationCompat.DEFAULT_ALL) + .setPriority(Notification.PRIORITY_DEFAULT); + return new ForegroundInfo(FETCH_NOTIFICATION_CHANNEL_ID, notificationBuilder.build()); + } + + @NonNull + @Override + public Result doWork() { + + setForegroundAsync(createForegroundInfo()); + Data outputData; + String instance = getInputData().getString(Helper.ARG_INSTANCE); + String token = getInputData().getString(Helper.ARG_TOKEN); + String statusDraftId = getInputData().getString(Helper.ARG_STATUS_DRAFT_ID); + StatusDraft statusDraft; + try { + statusDraft = new StatusDraft(getApplicationContext()).geStatusDraft(statusDraftId); + Intent intent = new Intent(getApplicationContext(), PostMessageService.class); + intent.putExtra(Helper.ARG_STATUS_DRAFT, statusDraft); + intent.putExtra(Helper.ARG_INSTANCE, instance); + intent.putExtra(Helper.ARG_TOKEN, token); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + getApplicationContext().startForegroundService(intent); + } else { + getApplicationContext().startService(intent); + } + } catch (DBException e) { + e.printStackTrace(); + outputData = new Data.Builder().putString("WORK_RESULT", getApplicationContext().getString(R.string.toast_error)).build(); + return Result.failure(outputData); + } + + return Result.success(new Data.Builder().putString("WORK_RESULT", getApplicationContext().getString(R.string.toot_sent)).build()); + } +} diff --git a/app/src/main/java/app/fedilab/android/jobs/ScheduleBoostWorker.java b/app/src/main/java/app/fedilab/android/jobs/ScheduleBoostWorker.java new file mode 100644 index 000000000..43e66c684 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/jobs/ScheduleBoostWorker.java @@ -0,0 +1,127 @@ +package app.fedilab.android.jobs; +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.content.Context; +import android.graphics.BitmapFactory; +import android.os.Build; + +import androidx.annotation.NonNull; +import androidx.core.app.NotificationCompat; +import androidx.work.Data; +import androidx.work.ForegroundInfo; +import androidx.work.Worker; +import androidx.work.WorkerParameters; + +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +import app.fedilab.android.R; +import app.fedilab.android.client.entities.ScheduledBoost; +import app.fedilab.android.client.mastodon.MastodonStatusesService; +import app.fedilab.android.client.mastodon.entities.Status; +import app.fedilab.android.exception.DBException; +import app.fedilab.android.helper.Helper; +import okhttp3.OkHttpClient; +import retrofit2.Call; +import retrofit2.Response; +import retrofit2.Retrofit; +import retrofit2.converter.gson.GsonConverterFactory; + +public class ScheduleBoostWorker extends Worker { + + private static final int NOTIFICATION_INT_CHANNEL_ID = 2; + private static final String CHANNEL_ID = "schedule_boost"; + private final NotificationManager notificationManager; + + public ScheduleBoostWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) { + super(context, workerParams); + notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + } + + @NonNull + private ForegroundInfo createForegroundInfo() { + if (Build.VERSION.SDK_INT >= 26) { + String channelName = "Boost messages"; + String channelDescription = "Schedule boosts channel"; + NotificationChannel notifChannel = new NotificationChannel(CHANNEL_ID, channelName, NotificationManager.IMPORTANCE_HIGH); + notifChannel.setDescription(channelDescription); + notificationManager.createNotificationChannel(notifChannel); + + } + NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(getApplicationContext(), CHANNEL_ID); + notificationBuilder.setSmallIcon(R.drawable.ic_notification) + .setLargeIcon(BitmapFactory.decodeResource(getApplicationContext().getResources(), R.drawable.ic_launcher_foreground)) + .setContentTitle(getApplicationContext().getString(R.string.schedule_boost)) + .setContentText(getApplicationContext().getString(R.string.schedule_boost)) + .setDefaults(NotificationCompat.DEFAULT_ALL) + .setPriority(Notification.PRIORITY_DEFAULT); + return new ForegroundInfo(NOTIFICATION_INT_CHANNEL_ID, notificationBuilder.build()); + } + + + private OkHttpClient getOkHttpClient() { + return new OkHttpClient.Builder() + .readTimeout(60, TimeUnit.SECONDS) + .connectTimeout(60, TimeUnit.SECONDS) + .proxy(Helper.getProxy(getApplicationContext())) + .build(); + } + + private MastodonStatusesService init(@NonNull String instance) { + Retrofit retrofit = new Retrofit.Builder() + .baseUrl("https://" + instance + "/api/v1/") + .addConverterFactory(GsonConverterFactory.create()) + .client(getOkHttpClient()) + .build(); + return retrofit.create(MastodonStatusesService.class); + } + + @NonNull + @Override + public Result doWork() { + + setForegroundAsync(createForegroundInfo()); + + String instance = getInputData().getString(Helper.ARG_INSTANCE); + String token = getInputData().getString(Helper.ARG_TOKEN); + String statusId = getInputData().getString(Helper.ARG_STATUS_ID); + String userID = getInputData().getString(Helper.ARG_USER_ID); + Data outputData = new Data.Builder().putString("WORK_RESULT", getApplicationContext().getString(R.string.toast_error)).build(); + if (instance != null) { + MastodonStatusesService mastodonStatusesService = init(instance); + Call statusCall = mastodonStatusesService.reblog(token, statusId, null); + if (statusCall != null) { + try { + Response statusResponse = statusCall.execute(); + if (statusResponse.isSuccessful()) { + try { + new ScheduledBoost(getApplicationContext()).removeScheduled(instance, userID, statusId); + outputData = new Data.Builder().putString("WORK_RESULT", getApplicationContext().getString(R.string.notif_reblog)).build(); + } catch (DBException e) { + e.printStackTrace(); + } + } + } catch (IOException e) { + e.printStackTrace(); + } + } + } + return Result.success(outputData); + } +} diff --git a/app/src/main/java/app/fedilab/android/jobs/ScheduleThreadWorker.java b/app/src/main/java/app/fedilab/android/jobs/ScheduleThreadWorker.java new file mode 100644 index 000000000..202b1e428 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/jobs/ScheduleThreadWorker.java @@ -0,0 +1,98 @@ +package app.fedilab.android.jobs; +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.content.Context; +import android.content.Intent; +import android.graphics.BitmapFactory; +import android.os.Build; + +import androidx.annotation.NonNull; +import androidx.core.app.NotificationCompat; +import androidx.work.Data; +import androidx.work.ForegroundInfo; +import androidx.work.Worker; +import androidx.work.WorkerParameters; + +import app.fedilab.android.R; +import app.fedilab.android.client.entities.StatusDraft; +import app.fedilab.android.exception.DBException; +import app.fedilab.android.helper.Helper; +import app.fedilab.android.services.PostMessageService; + +public class ScheduleThreadWorker extends Worker { + + private static final int NOTIFICATION_INT_CHANNEL_ID = 3; + private static final String CHANNEL_ID = "scheduled_thread"; + private final NotificationManager notificationManager; + + public ScheduleThreadWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) { + super(context, workerParams); + notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + } + + @NonNull + private ForegroundInfo createForegroundInfo() { + if (Build.VERSION.SDK_INT >= 26) { + String channelName = "Scheduled threads"; + String channelDescription = "Scheduled threads channel"; + NotificationChannel notifChannel = new NotificationChannel(CHANNEL_ID, channelName, NotificationManager.IMPORTANCE_HIGH); + notifChannel.setDescription(channelDescription); + notificationManager.createNotificationChannel(notifChannel); + + } + NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(getApplicationContext(), CHANNEL_ID); + notificationBuilder.setSmallIcon(R.drawable.ic_notification) + .setLargeIcon(BitmapFactory.decodeResource(getApplicationContext().getResources(), R.drawable.ic_launcher_foreground)) + .setContentTitle(getApplicationContext().getString(R.string.scheduled_toots)) + .setContentText(getApplicationContext().getString(R.string.scheduled_toots)) + .setDefaults(NotificationCompat.DEFAULT_ALL) + .setPriority(Notification.PRIORITY_DEFAULT); + return new ForegroundInfo(NOTIFICATION_INT_CHANNEL_ID, notificationBuilder.build()); + } + + @NonNull + @Override + public Result doWork() { + + setForegroundAsync(createForegroundInfo()); + Data outputData; + String instance = getInputData().getString(Helper.ARG_INSTANCE); + String token = getInputData().getString(Helper.ARG_TOKEN); + String statusDraftId = getInputData().getString(Helper.ARG_STATUS_DRAFT_ID); + StatusDraft statusDraft; + try { + statusDraft = new StatusDraft(getApplicationContext()).geStatusDraft(statusDraftId); + Intent intent = new Intent(getApplicationContext(), PostMessageService.class); + intent.putExtra(Helper.ARG_STATUS_DRAFT, statusDraft); + intent.putExtra(Helper.ARG_INSTANCE, instance); + intent.putExtra(Helper.ARG_TOKEN, token); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + getApplicationContext().startForegroundService(intent); + } else { + getApplicationContext().startService(intent); + } + } catch (DBException e) { + e.printStackTrace(); + outputData = new Data.Builder().putString("WORK_RESULT", getApplicationContext().getString(R.string.toast_error)).build(); + return Result.failure(outputData); + } + + return Result.success(new Data.Builder().putString("WORK_RESULT", getApplicationContext().getString(R.string.toot_sent)).build()); + } +} diff --git a/app/src/main/java/app/fedilab/android/services/CustomReceiver.java b/app/src/main/java/app/fedilab/android/services/CustomReceiver.java new file mode 100644 index 000000000..d751fd3e4 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/services/CustomReceiver.java @@ -0,0 +1,88 @@ +package app.fedilab.android.services; +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import android.content.Context; +import android.content.Intent; +import android.util.Log; + +import androidx.annotation.NonNull; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.unifiedpush.android.connector.MessagingReceiver; + +import app.fedilab.android.helper.Helper; +import app.fedilab.android.helper.NotificationsHelper; +import app.fedilab.android.helper.PushNotifications; + + +public class CustomReceiver extends MessagingReceiver { + + + public CustomReceiver() { + super(); + } + + + @Override + public void onMessage(@NotNull Context context, @NotNull byte[] message, @NotNull String slug) { + // Called when a new message is received. The message contains the full POST body of the push message + Log.v(Helper.TAG, "onMessage: " + slug); + new Thread(() -> { + try { + /* ECDH ecdh = ECDH.getInstance(slug); + Log.v(Helper.TAG, "ecdh: " + ecdh); + if (ecdh == null) { + return; + }*/ + //String decrypted = ecdh.uncryptMessage(context, String.valueOf(message)); + // Log.v(Helper.TAG, "decrypted: " + decrypted); + NotificationsHelper.task(context, slug); + // Log.v(Helper.TAG, "decrypted: " + decrypted); + } catch (Exception e) { + e.printStackTrace(); + } + + + }).start(); + } + + @Override + public void onReceive(@NonNull Context context, @NonNull Intent intent) { + super.onReceive(context, intent); + } + + @Override + public void onNewEndpoint(@Nullable Context context, @NotNull String endpoint, @NotNull String slug) { + if (context != null) { + PushNotifications + .registerPushNotifications(context, endpoint, slug); + } + Log.v(Helper.TAG, "onNewEndpoint: " + slug); + } + + + @Override + public void onRegistrationFailed(@Nullable Context context, @NotNull String s) { + Log.v(Helper.TAG, "onRegistrationFailed: " + s); + } + + @Override + public void onUnregistered(@Nullable Context context, @NotNull String s) { + Log.v(Helper.TAG, "onUnregistered: " + s); + } +} + diff --git a/app/src/main/java/app/fedilab/android/services/PostMessageService.java b/app/src/main/java/app/fedilab/android/services/PostMessageService.java new file mode 100644 index 000000000..430ff3ac6 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/services/PostMessageService.java @@ -0,0 +1,289 @@ +package app.fedilab.android.services; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import android.app.IntentService; +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.content.Context; +import android.content.Intent; +import android.graphics.BitmapFactory; +import android.os.Build; +import android.os.Bundle; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.app.NotificationCompat; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import app.fedilab.android.BaseMainActivity; +import app.fedilab.android.R; +import app.fedilab.android.client.entities.PostState; +import app.fedilab.android.client.entities.StatusDraft; +import app.fedilab.android.client.mastodon.MastodonStatusesService; +import app.fedilab.android.client.mastodon.entities.Attachment; +import app.fedilab.android.client.mastodon.entities.Poll; +import app.fedilab.android.client.mastodon.entities.ScheduledStatus; +import app.fedilab.android.client.mastodon.entities.Status; +import app.fedilab.android.exception.DBException; +import app.fedilab.android.helper.Helper; +import okhttp3.MultipartBody; +import okhttp3.OkHttpClient; +import retrofit2.Call; +import retrofit2.Response; +import retrofit2.Retrofit; +import retrofit2.converter.gson.GsonConverterFactory; + +public class PostMessageService extends IntentService { + + private static final int NOTIFICATION_INT_CHANNEL_ID = 1; + public static String CHANNEL_ID = "post_messages"; + private long totalMediaSize; + private long totalBitRead; + private NotificationCompat.Builder notificationBuilder; + private NotificationManager notificationManager; + private int messageToSend; + private int messageSent; + + /** + * @param name - String + * @deprecated + */ + public PostMessageService(String name) { + super(name); + } + + @SuppressWarnings("unused") + public PostMessageService() { + super("PostMessageService"); + } + + @Override + public void onCreate() { + super.onCreate(); + notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + if (Build.VERSION.SDK_INT >= 26) { + String channelName = "Post messages"; + String channelDescription = "Post messages in background"; + NotificationChannel notifChannel = new NotificationChannel(CHANNEL_ID, channelName, NotificationManager.IMPORTANCE_HIGH); + notifChannel.setDescription(channelDescription); + notificationManager.createNotificationChannel(notifChannel); + + } + notificationBuilder = new NotificationCompat.Builder(getBaseContext(), CHANNEL_ID); + notificationBuilder.setSmallIcon(R.drawable.ic_notification) + .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher_foreground)) + .setContentTitle(getString(R.string.post_message)) + .setContentText(getString(R.string.post_message_text, messageSent, messageToSend)) + .setDefaults(NotificationCompat.DEFAULT_ALL) + .setPriority(Notification.PRIORITY_DEFAULT); + startForeground(NOTIFICATION_INT_CHANNEL_ID, notificationBuilder.build()); + } + + + private OkHttpClient getOkHttpClient() { + return new OkHttpClient.Builder() + .readTimeout(120, TimeUnit.SECONDS) + .connectTimeout(120, TimeUnit.SECONDS) + .proxy(Helper.getProxy(getApplication().getApplicationContext())) + .build(); + } + + private MastodonStatusesService init(@NonNull String instance) { + Retrofit retrofit = new Retrofit.Builder() + .baseUrl("https://" + instance + "/api/v1/") + .addConverterFactory(GsonConverterFactory.create()) + .client(getOkHttpClient()) + .build(); + return retrofit.create(MastodonStatusesService.class); + } + + @Override + protected void onHandleIntent(@Nullable Intent intent) { + StatusDraft statusDraft = null; + String token = null, instance = null; + String scheduledDate = null; + if (intent != null && intent.getExtras() != null) { + Bundle b = intent.getExtras(); + statusDraft = (StatusDraft) b.getSerializable(Helper.ARG_STATUS_DRAFT); + token = b.getString(Helper.ARG_TOKEN); + instance = b.getString(Helper.ARG_INSTANCE); + scheduledDate = b.getString(Helper.ARG_SCHEDULED_DATE); + } + //Should not be null, but a simple security + if (token == null) { + token = BaseMainActivity.currentToken; + } + if (instance == null) { + instance = BaseMainActivity.currentInstance; + } + MastodonStatusesService mastodonStatusesService = init(instance); + boolean error = false; + if (statusDraft != null && statusDraft.statusDraftList != null && statusDraft.statusDraftList.size() > 0) { + //If state is null, it is created (typically when submitting the status the first time) + if (statusDraft.state == null) { + statusDraft.state = new PostState(); + statusDraft.state.posts = new ArrayList<>(); + statusDraft.state.number_of_posts = statusDraft.statusDraftList.size(); + for (Status status : statusDraft.statusDraftList) { + PostState.Post post = new PostState.Post(); + post.number_of_media = status.media_attachments != null ? status.media_attachments.size() : 0; + statusDraft.state.posts.add(post); + } + } + //Check if previous messages in thread have already been published (ie: when resending after a fail) + int startingPosition = 0; + for (PostState.Post post : statusDraft.state.posts) { + if (post.id == null) { + break; + } + startingPosition++; + } + String in_reply_to_status = null; + List statuses = statusDraft.statusDraftList; + totalMediaSize = 0; + totalBitRead = 0; + for (int i = startingPosition; i < statuses.size(); i++) { + if (statuses.get(i).media_attachments != null && statuses.get(i).media_attachments.size() > 0) { + for (Attachment attachment : statuses.get(i).media_attachments) { + totalMediaSize += attachment.size; + } + } + } + messageToSend = statuses.size() - startingPosition; + messageSent = 0; + for (int i = startingPosition; i < statuses.size(); i++) { + if (notificationBuilder != null) { + notificationBuilder.setProgress(100, messageSent * 100 / messageToSend, true); + notificationBuilder.setContentText(getString(R.string.post_message_text, messageSent, messageToSend)); + notificationManager.notify(NOTIFICATION_INT_CHANNEL_ID, notificationBuilder.build()); + } + //post media first + List attachmentIds = null; + if (statuses.get(i).media_attachments != null && statuses.get(i).media_attachments.size() > 0) { + attachmentIds = new ArrayList<>(); + for (Attachment attachment : statuses.get(i).media_attachments) { + MultipartBody.Part fileMultipartBody; + fileMultipartBody = Helper.getMultipartBody("file", attachment); + Call attachmentCall = mastodonStatusesService.postMedia(token, fileMultipartBody, null, attachment.description, null); + if (attachmentCall != null) { + try { + Response attachmentResponse = attachmentCall.execute(); + if (attachmentResponse.isSuccessful()) { + Attachment attachmentReply = attachmentResponse.body(); + if (attachmentReply != null) { + attachmentIds.add(attachmentReply.id); + } + } + } catch (IOException e) { + error = true; + e.printStackTrace(); + } + } + } + } + List poll_options = null; + Integer poll_expire_in = null; + Boolean poll_multiple = null; + Boolean poll_hide_totals = null; + if (statuses.get(i).poll != null) { + poll_options = new ArrayList<>(); + for (Poll.PollItem pollItem : statuses.get(i).poll.options) { + poll_options.add(pollItem.title); + } + poll_expire_in = statuses.get(i).poll.expire_in; + poll_multiple = statuses.get(i).poll.multiple; + poll_hide_totals = false; + } + Call statusCall; + if (scheduledDate == null) { + statusCall = mastodonStatusesService.createStatus(null, token, statuses.get(i).text, attachmentIds, poll_options, poll_expire_in, + poll_multiple, poll_hide_totals, in_reply_to_status, statuses.get(i).sensitive, statuses.get(i).spoiler_text, statuses.get(i).visibility.toLowerCase(), statuses.get(i).language); + try { + Response statusResponse = statusCall.execute(); + + if (statusResponse.isSuccessful()) { + Status statusReply = statusResponse.body(); + if (statusReply != null) { + in_reply_to_status = statusReply.id; + statusDraft.state.posts_successfully_sent = i; + statusDraft.state.posts.get(i).id = statusReply.id; + statusDraft.state.posts.get(i).in_reply_to_id = statusReply.in_reply_to_id; + try { + new StatusDraft(getApplicationContext()).updatePostState(statusDraft); + } catch (DBException e) { + e.printStackTrace(); + } + if (!error && i >= statusDraft.statusDraftList.size()) { + try { + new StatusDraft(PostMessageService.this).removeDraft(statusDraft); + } catch (DBException e) { + e.printStackTrace(); + } + stopSelf(); + } + } + } + } catch (IOException e) { + e.printStackTrace(); + error = true; + } + } else { + Call scheduledStatusCall = mastodonStatusesService.createScheduledStatus(null, token, statuses.get(i).text, attachmentIds, poll_options, poll_expire_in, + poll_multiple, poll_hide_totals, in_reply_to_status, statuses.get(i).sensitive, statuses.get(i).spoiler_text, statuses.get(i).visibility.toLowerCase(), scheduledDate, statuses.get(i).language); + try { + Response statusResponse = scheduledStatusCall.execute(); + + if (statusResponse.isSuccessful()) { + ScheduledStatus statusReply = statusResponse.body(); + if (statusReply != null) { + in_reply_to_status = statusReply.id; + statusDraft.state.posts_successfully_sent = i; + statusDraft.state.posts.get(i).id = statusReply.id; + statusDraft.state.posts.get(i).in_reply_to_id = statusReply.params.in_reply_to_id; + try { + new StatusDraft(getApplicationContext()).updatePostState(statusDraft); + } catch (DBException e) { + e.printStackTrace(); + } + if (!error && i >= statusDraft.statusDraftList.size()) { + try { + new StatusDraft(PostMessageService.this).removeDraft(statusDraft); + } catch (DBException e) { + e.printStackTrace(); + } + stopSelf(); + } + } + } + } catch (IOException e) { + e.printStackTrace(); + error = true; + } + } + messageSent++; + if (messageSent > messageToSend) { + messageSent = messageToSend; + } + } + } + } + +} diff --git a/app/src/main/java/app/fedilab/android/services/PostMessageServiceSave.java b/app/src/main/java/app/fedilab/android/services/PostMessageServiceSave.java new file mode 100644 index 000000000..d55089c3f --- /dev/null +++ b/app/src/main/java/app/fedilab/android/services/PostMessageServiceSave.java @@ -0,0 +1,278 @@ +package app.fedilab.android.services; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import android.app.IntentService; +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.content.Context; +import android.content.Intent; +import android.graphics.BitmapFactory; +import android.os.Build; +import android.os.Bundle; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.app.NotificationCompat; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import app.fedilab.android.BaseMainActivity; +import app.fedilab.android.R; +import app.fedilab.android.client.entities.PostState; +import app.fedilab.android.client.entities.StatusDraft; +import app.fedilab.android.client.mastodon.MastodonStatusesService; +import app.fedilab.android.client.mastodon.entities.Attachment; +import app.fedilab.android.client.mastodon.entities.Poll; +import app.fedilab.android.client.mastodon.entities.ScheduledStatus; +import app.fedilab.android.client.mastodon.entities.Status; +import app.fedilab.android.exception.DBException; +import app.fedilab.android.helper.Helper; +import okhttp3.MultipartBody; +import okhttp3.OkHttpClient; +import retrofit2.Call; +import retrofit2.Response; +import retrofit2.Retrofit; +import retrofit2.converter.gson.GsonConverterFactory; + +public class PostMessageServiceSave extends IntentService { + + private static final int NOTIFICATION_INT_CHANNEL_ID = 1; + public static String CHANNEL_ID = "post_messages"; + private long totalMediaSize; + private long totalBitRead; + private NotificationCompat.Builder notificationBuilder; + private NotificationManager notificationManager; + private int messageToSend; + private int messageSent; + + /** + * @param name - String + * @deprecated + */ + public PostMessageServiceSave(String name) { + super(name); + } + + @SuppressWarnings("unused") + public PostMessageServiceSave() { + super("PostMessageService"); + } + + @Override + public void onCreate() { + super.onCreate(); + notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + if (Build.VERSION.SDK_INT >= 26) { + String channelName = "Post messages"; + String channelDescription = "Post messages in background"; + NotificationChannel notifChannel = new NotificationChannel(CHANNEL_ID, channelName, NotificationManager.IMPORTANCE_HIGH); + notifChannel.setDescription(channelDescription); + notificationManager.createNotificationChannel(notifChannel); + + } + notificationBuilder = new NotificationCompat.Builder(getBaseContext(), CHANNEL_ID); + notificationBuilder.setSmallIcon(R.drawable.ic_notification) + .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher_foreground)) + .setContentTitle(getString(R.string.post_message)) + .setContentText(getString(R.string.post_message_text, messageSent, messageToSend)) + .setDefaults(NotificationCompat.DEFAULT_ALL) + .setPriority(Notification.PRIORITY_DEFAULT); + startForeground(NOTIFICATION_INT_CHANNEL_ID, notificationBuilder.build()); + } + + + private OkHttpClient getOkHttpClient() { + return new OkHttpClient.Builder() + .readTimeout(120, TimeUnit.SECONDS) + .connectTimeout(120, TimeUnit.SECONDS) + .proxy(Helper.getProxy(getApplication().getApplicationContext())) + .build(); + } + + private MastodonStatusesService init(@NonNull String instance) { + Retrofit retrofit = new Retrofit.Builder() + .baseUrl("https://" + instance + "/api/v1/") + .addConverterFactory(GsonConverterFactory.create()) + .client(getOkHttpClient()) + .build(); + return retrofit.create(MastodonStatusesService.class); + } + + @Override + protected void onHandleIntent(@Nullable Intent intent) { + StatusDraft statusDraft = null; + String token = null, instance = null; + String scheduledDate = null; + if (intent != null && intent.getExtras() != null) { + Bundle b = intent.getExtras(); + statusDraft = (StatusDraft) b.getSerializable(Helper.ARG_STATUS_DRAFT); + token = b.getString(Helper.ARG_TOKEN); + instance = b.getString(Helper.ARG_INSTANCE); + scheduledDate = b.getString(Helper.ARG_SCHEDULED_DATE); + } + //Should not be null, but a simple security + if (token == null) { + token = BaseMainActivity.currentToken; + } + if (instance == null) { + instance = BaseMainActivity.currentInstance; + } + MastodonStatusesService mastodonStatusesService = init(instance); + boolean error = false; + if (statusDraft != null && statusDraft.statusDraftList != null && statusDraft.statusDraftList.size() > 0) { + //If state is null, it is created (typically when submitting the status the first time) + if (statusDraft.state == null) { + statusDraft.state = new PostState(); + statusDraft.state.posts = new ArrayList<>(); + statusDraft.state.number_of_posts = statusDraft.statusDraftList.size(); + for (Status status : statusDraft.statusDraftList) { + PostState.Post post = new PostState.Post(); + post.number_of_media = status.media_attachments != null ? status.media_attachments.size() : 0; + statusDraft.state.posts.add(post); + } + } + //Check if previous messages in thread have already been published (ie: when resending after a fail) + int startingPosition = 0; + for (PostState.Post post : statusDraft.state.posts) { + if (post.id == null) { + break; + } + startingPosition++; + } + String in_reply_to_status = null; + List statuses = statusDraft.statusDraftList; + totalMediaSize = 0; + totalBitRead = 0; + for (int i = startingPosition; i < statuses.size(); i++) { + if (statuses.get(i).media_attachments != null && statuses.get(i).media_attachments.size() > 0) { + for (Attachment attachment : statuses.get(i).media_attachments) { + totalMediaSize += attachment.size; + } + } + } + messageToSend = statuses.size() - startingPosition; + messageSent = 0; + for (int i = startingPosition; i < statuses.size(); i++) { + if (notificationBuilder != null) { + notificationBuilder.setProgress(100, messageSent * 100 / messageToSend, true); + notificationBuilder.setContentText(getString(R.string.post_message_text, messageSent, messageToSend)); + notificationManager.notify(NOTIFICATION_INT_CHANNEL_ID, notificationBuilder.build()); + } + //post media first + List attachmentIds = null; + if (statuses.get(i).media_attachments != null && statuses.get(i).media_attachments.size() > 0) { + attachmentIds = new ArrayList<>(); + for (Attachment attachment : statuses.get(i).media_attachments) { + MultipartBody.Part fileMultipartBody; + fileMultipartBody = Helper.getMultipartBody("file", attachment); + Call attachmentCall = mastodonStatusesService.postMedia(token, fileMultipartBody, null, attachment.description, null); + if (attachmentCall != null) { + try { + Response attachmentResponse = attachmentCall.execute(); + if (attachmentResponse.isSuccessful()) { + Attachment attachmentReply = attachmentResponse.body(); + if (attachmentReply != null) { + attachmentIds.add(attachmentReply.id); + } + } + } catch (IOException e) { + error = true; + e.printStackTrace(); + } + } + } + } + List poll_options = null; + Integer poll_expire_in = null; + Boolean poll_multiple = null; + Boolean poll_hide_totals = null; + if (statuses.get(i).poll != null) { + poll_options = new ArrayList<>(); + for (Poll.PollItem pollItem : statuses.get(i).poll.options) { + poll_options.add(pollItem.title); + } + poll_expire_in = statuses.get(i).poll.expire_in; + poll_multiple = statuses.get(i).poll.multiple; + poll_hide_totals = false; + } + if (scheduledDate == null) { + Call statusCall = mastodonStatusesService.createStatus(null, token, statuses.get(i).text, attachmentIds, poll_options, poll_expire_in, + poll_multiple, poll_hide_totals, in_reply_to_status, statuses.get(i).sensitive, statuses.get(i).spoiler_text, statuses.get(i).visibility.toLowerCase(), statuses.get(i).language); + if (statusCall != null) { + try { + Response statusResponse = statusCall.execute(); + + if (statusResponse.isSuccessful()) { + Status statusReply = statusResponse.body(); + if (statusReply != null) { + in_reply_to_status = statusReply.id; + statusDraft.state.posts_successfully_sent = i; + statusDraft.state.posts.get(i).id = statusReply.id; + statusDraft.state.posts.get(i).in_reply_to_id = statusReply.in_reply_to_id; + try { + new StatusDraft(getApplicationContext()).updatePostState(statusDraft); + } catch (DBException e) { + e.printStackTrace(); + } + if (!error && i >= statusDraft.statusDraftList.size()) { + try { + new StatusDraft(PostMessageServiceSave.this).removeDraft(statusDraft); + } catch (DBException e) { + e.printStackTrace(); + } + stopSelf(); + } + } + } + } catch (IOException e) { + e.printStackTrace(); + error = true; + } + } + } else { + //Even if we use the loop, there is always one status to schedule from server side. + Call scheduledStatusCall = mastodonStatusesService.createScheduledStatus(null, token, statuses.get(i).text, attachmentIds, poll_options, poll_expire_in, + poll_multiple, poll_hide_totals, in_reply_to_status, statuses.get(i).sensitive, statuses.get(i).spoiler_text, statuses.get(i).visibility.toLowerCase(), scheduledDate, statuses.get(i).language); + if (scheduledStatusCall != null) { + try { + Response statusResponse = scheduledStatusCall.execute(); + if (statusResponse.isSuccessful()) { + try { + new StatusDraft(PostMessageServiceSave.this).removeDraft(statusDraft); + } catch (DBException e) { + e.printStackTrace(); + } + stopSelf(); + } + } catch (IOException e) { + e.printStackTrace(); + error = true; + } + } + } + messageSent++; + if (messageSent > messageToSend) { + messageSent = messageToSend; + } + } + } + } + +} diff --git a/app/src/main/java/app/fedilab/android/sqlite/Sqlite.java b/app/src/main/java/app/fedilab/android/sqlite/Sqlite.java new file mode 100644 index 000000000..ccfe6bb9a --- /dev/null +++ b/app/src/main/java/app/fedilab/android/sqlite/Sqlite.java @@ -0,0 +1,204 @@ +package app.fedilab.android.sqlite; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + + +import android.content.Context; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; + + +public class Sqlite extends SQLiteOpenHelper { + + + public static final int DB_VERSION = 1; + public static final String DB_NAME = "fedilab_db"; + + //Table of owned accounts + public static final String TABLE_USER_ACCOUNT = "USER_ACCOUNT"; + + + public static final String COL_USER_ID = "USER_ID"; + public static final String COL_INSTANCE = "INSTANCE"; + public static final String COL_API = "API"; + public static final String COL_SOFTWARE = "SOFTWARE"; + public static final String COL_TOKEN = "TOKEN"; + public static final String COL_REFRESH_TOKEN = "REFRESH_TOKEN"; + public static final String COL_TOKEN_VALIDITY = "TOKEN_VALIDITY"; + public static final String COL_ACCOUNT = "ACCOUNT"; + public static final String COL_APP_CLIENT_SECRET = "APP_CLIENT_SECRET"; + public static final String COL_APP_CLIENT_ID = "APP_CLIENT_ID"; + public static final String COL_CREATED_AT = "CREATED_AT"; + public static final String COL_UPDATED_AT = "UPDATED_AT"; + //Table for timelines + public static final String TABLE_TIMELINES = "TIMELINES"; + public static final String COL_ID = "ID"; + public static final String COL_TYPE = "TYPE"; + public static final String COL_DISPLAYED = "DISPLAYED"; + public static final String COL_POSITION = "POSITION"; + public static final String COL_REMOTE_INSTANCE = "REMOTE_INSTANCE"; + public static final String COL_TIMELINE_OPTION = "TIMELINE_OPTION"; + //Table for cache + public static final String TABLE_STATUS_CACHE = "STATUS_CACHE"; + public static final String COL_STATUS_ID = "STATUS_ID"; + public static final String COL_STATUS = "STATUS"; + //Table for emojis + public static final String TABLE_EMOJI_INSTANCE = "EMOJI_INSTANCE"; + public static final String COL_EMOJI_LIST = "EMOJI_LIST"; + //Table for instance info + public static final String TABLE_INSTANCE_INFO = "INSTANCE_INFO"; + public static final String COL_INFO = "INFO"; + //Table for instance drafts + public static final String TABLE_STATUS_DRAFT = "STATUS_DRAFT"; + public static final String COL_DRAFTS = "DRAFTS"; + public static final String COL_REPLIES = "REPLIES"; + public static final String COL_STATE = "STATE"; + //Table pinned timelines + public static final String TABLE_PINNED_TIMELINES = "PINNED_TIMELINES"; + public static final String COL_PINNED_TIMELINES = "PINNED_TIMELINES"; + //Schedule boost + public static final String TABLE_SCHEDULE_BOOST = "SCHEDULE_BOOST"; + public static final String COL_SCHEDULED_AT = "SCHEDULED_AT"; + public static final String COL_REBLOGGED = "REBLOGGED"; + public static final String COL_WORKER_UUID = "WORKER_UUID"; + + private static final String CREATE_TABLE_USER_ACCOUNT = "CREATE TABLE " + TABLE_USER_ACCOUNT + " (" + + COL_USER_ID + " TEXT NOT NULL, " + + COL_INSTANCE + " TEXT NOT NULL, " + + COL_API + " TEXT NOT NULL, " + + COL_SOFTWARE + " TEXT, " + + COL_TOKEN + " TEXT NOT NULL, " + + COL_REFRESH_TOKEN + " TEXT, " + + COL_TOKEN_VALIDITY + " INTEGER, " + + COL_ACCOUNT + " TEXT NOT NULL, " + + COL_APP_CLIENT_ID + " TEXT NOT NULL, " + + COL_APP_CLIENT_SECRET + " TEXT NOT NULL, " + + COL_CREATED_AT + " TEXT NOT NULL," + + COL_UPDATED_AT + " TEXT)"; + private static final String CREATE_TABLE_TIMELINES = "CREATE TABLE IF NOT EXISTS " + TABLE_TIMELINES + " (" + + COL_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + + COL_POSITION + " INTEGER NOT NULL, " + + COL_USER_ID + " TEXT NOT NULL, " + COL_INSTANCE + " TEXT NOT NULL, " + + COL_TYPE + " TEXT NOT NULL, " + + COL_REMOTE_INSTANCE + " TEXT, " + + COL_DISPLAYED + " INTEGER NOT NULL, " + + COL_TIMELINE_OPTION + " TEXT)"; + private static final String CREATE_TABLE_STATUS_CACHE = "CREATE TABLE IF NOT EXISTS " + TABLE_STATUS_CACHE + " (" + + COL_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + + COL_USER_ID + " TEXT NOT NULL, " + COL_INSTANCE + " TEXT NOT NULL, " + + COL_TYPE + " TEXT NOT NULL, " + + COL_STATUS_ID + " TEXT NOT NULL, " + + COL_STATUS + " TEXT NOT NULL, " + + COL_CREATED_AT + " TEXT NOT NULL," + + COL_UPDATED_AT + " TEXT)"; + private static final String CREATE_TABLE_EMOJI_INSTANCE = "CREATE TABLE IF NOT EXISTS " + TABLE_EMOJI_INSTANCE + " (" + + COL_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + + COL_INSTANCE + " TEXT NOT NULL, " + + COL_EMOJI_LIST + " TEXT)"; + + private static final String CREATE_TABLE_INSTANCE_INFO = "CREATE TABLE IF NOT EXISTS " + TABLE_INSTANCE_INFO + " (" + + COL_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + + COL_INSTANCE + " TEXT NOT NULL, " + + COL_INFO + " TEXT)"; + + + private static final String CREATE_TABLE_STATUS_DRAFT = "CREATE TABLE IF NOT EXISTS " + TABLE_STATUS_DRAFT + " (" + + COL_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + + COL_INSTANCE + " TEXT NOT NULL, " + + COL_USER_ID + " TEXT NOT NULL, " + + COL_DRAFTS + " TEXT NOT NULL, " + + COL_REPLIES + " TEXT, " + + COL_STATE + " TEXT, " + + COL_WORKER_UUID + " TEXT, " + + COL_CREATED_AT + " TEXT NOT NULL, " + + COL_UPDATED_AT + " TEXT, " + + COL_SCHEDULED_AT + " TEXT)"; + + private static final String CREATE_TABLE_PINNED_TIMELINES = "CREATE TABLE IF NOT EXISTS " + TABLE_PINNED_TIMELINES + " (" + + COL_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + + COL_INSTANCE + " TEXT NOT NULL, " + + COL_USER_ID + " TEXT NOT NULL, " + + COL_PINNED_TIMELINES + " TEXT NOT NULL, " + + COL_CREATED_AT + " TEXT NOT NULL, " + + COL_UPDATED_AT + " TEXT)"; + + + private static final String CREATE_TABLE_SCHEDULE_BOOST = "CREATE TABLE IF NOT EXISTS " + TABLE_SCHEDULE_BOOST + " (" + + COL_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + + COL_INSTANCE + " TEXT NOT NULL, " + + COL_USER_ID + " TEXT NOT NULL, " + + COL_STATUS_ID + " TEXT NOT NULL, " + + COL_STATUS + " TEXT NOT NULL, " + + COL_REBLOGGED + " INTEGER, " + + COL_WORKER_UUID + " TEXT, " + + COL_SCHEDULED_AT + " TEXT NOT NULL)"; + + public static SQLiteDatabase db; + private static Sqlite sInstance; + + public Sqlite(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) { + super(context, name, factory, version); + } + + + public static synchronized Sqlite getInstance(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) { + if (sInstance == null) { + sInstance = new Sqlite(context, name, factory, version); + } + return sInstance; + } + + + @Override + public void onCreate(SQLiteDatabase db) { + db.execSQL(CREATE_TABLE_USER_ACCOUNT); + db.execSQL(CREATE_TABLE_TIMELINES); + db.execSQL(CREATE_TABLE_STATUS_CACHE); + db.execSQL(CREATE_TABLE_EMOJI_INSTANCE); + db.execSQL(CREATE_TABLE_INSTANCE_INFO); + db.execSQL(CREATE_TABLE_STATUS_DRAFT); + db.execSQL(CREATE_TABLE_PINNED_TIMELINES); + db.execSQL(CREATE_TABLE_SCHEDULE_BOOST); + } + + @Override + public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { + + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + //noinspection SwitchStatementWithTooFewBranches + switch (oldVersion) { + default: + break; + } + } + + public SQLiteDatabase open() { + //opened with write access + db = getWritableDatabase(); + return db; + } + + public void close() { + //Close the db + if (db != null && db.isOpen()) { + db.close(); + } + } + + +} diff --git a/app/src/main/java/app/fedilab/android/ui/drawer/AccountAdapter.java b/app/src/main/java/app/fedilab/android/ui/drawer/AccountAdapter.java new file mode 100644 index 000000000..cb079e572 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/ui/drawer/AccountAdapter.java @@ -0,0 +1,272 @@ +package app.fedilab.android.ui.drawer; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.res.ColorStateList; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import androidx.core.app.ActivityOptionsCompat; +import androidx.core.content.ContextCompat; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.ViewModelProvider; +import androidx.lifecycle.ViewModelStoreOwner; +import androidx.preference.PreferenceManager; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.Date; +import java.util.List; + +import app.fedilab.android.BaseMainActivity; +import app.fedilab.android.R; +import app.fedilab.android.activities.ProfileActivity; +import app.fedilab.android.client.mastodon.entities.Account; +import app.fedilab.android.databinding.DrawerAccountBinding; +import app.fedilab.android.helper.Helper; +import app.fedilab.android.helper.MastodonHelper; +import app.fedilab.android.viewmodel.mastodon.AccountsVM; +import es.dmoral.toasty.Toasty; + + +public class AccountAdapter extends RecyclerView.Adapter { + + private static ProfileActivity.action doAction; + private final List accountList; + private Context context; + + public AccountAdapter(List accountList) { + this.accountList = accountList; + } + + public static void accountManagement(Context context, AccountViewHolder accountViewHolder, Account account, int position, RecyclerView.Adapter adapter) { + MastodonHelper.loadPPMastodon(accountViewHolder.binding.avatar, account); + + SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(context); + + AccountsVM accountsVM = new ViewModelProvider((ViewModelStoreOwner) context).get(AccountsVM.class); + + accountViewHolder.binding.avatar.setOnClickListener(v -> { + Intent intent = new Intent(context, ProfileActivity.class); + Bundle b = new Bundle(); + b.putSerializable(Helper.ARG_ACCOUNT, account); + intent.putExtras(b); + ActivityOptionsCompat options = ActivityOptionsCompat + .makeSceneTransitionAnimation((Activity) context, accountViewHolder.binding.avatar, context.getString(R.string.activity_porfile_pp)); + // start the new activity + context.startActivity(intent, options.toBundle()); + }); + + if (account.relationShip != null) { + + doAction = ProfileActivity.action.FOLLOW; + accountViewHolder.binding.followAction.setText(R.string.action_follow); + accountViewHolder.binding.followAction.setVisibility(View.VISIBLE); + accountViewHolder.binding.followAction.setEnabled(true); + + if (account.relationShip.id.compareToIgnoreCase(BaseMainActivity.currentUserID) == 0) { + doAction = ProfileActivity.action.NOTHING; + accountViewHolder.binding.followAction.setVisibility(View.GONE); + accountViewHolder.binding.muteGroup.setVisibility(View.GONE); + accountViewHolder.binding.block.setVisibility(View.GONE); + } else { + accountViewHolder.binding.followAction.setVisibility(View.VISIBLE); + accountViewHolder.binding.muteGroup.setVisibility(View.VISIBLE); + accountViewHolder.binding.block.setVisibility(View.VISIBLE); + } + + if (account.relationShip.following) { + accountViewHolder.binding.followAction.setBackgroundTintList(ColorStateList.valueOf(ContextCompat.getColor(context, R.color.red_1))); + doAction = ProfileActivity.action.UNFOLLOW; + accountViewHolder.binding.followAction.setIconResource(R.drawable.ic_baseline_person_remove_24); + } else if (account.relationShip.requested) { + doAction = ProfileActivity.action.NOTHING; + accountViewHolder.binding.followAction.setEnabled(false); + accountViewHolder.binding.followAction.setIconResource(R.drawable.ic_baseline_hourglass_full_24); + } + + + if (account.relationShip.blocking) { + accountViewHolder.binding.block.setChecked(true); + accountViewHolder.binding.block.setOnClickListener(v -> accountsVM.unblock(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, account.id) + .observe((LifecycleOwner) context, relationShip -> { + account.relationShip = relationShip; + adapter.notifyItemChanged(position); + })); + } else { + accountViewHolder.binding.block.setChecked(false); + accountViewHolder.binding.block.setOnClickListener(v -> accountsVM.block(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, account.id) + .observe((LifecycleOwner) context, relationShip -> { + account.relationShip = relationShip; + adapter.notifyItemChanged(position); + })); + } + if (account.mute_expires_at != null && account.mute_expires_at.after(new Date())) { + accountViewHolder.binding.muteTimed.setChecked(true); + accountViewHolder.binding.muteTimed.setOnClickListener(v -> accountsVM.unmute(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, account.id) + .observe((LifecycleOwner) context, relationShip -> { + account.relationShip = relationShip; + accountsVM.getMutes(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, "1", account.id, null) + .observe((LifecycleOwner) context, accounts -> { + if (accounts != null && accounts.accounts != null && accounts.accounts.size() > 0) { + account.mute_expires_at = accounts.accounts.get(0).mute_expires_at; + adapter.notifyItemChanged(position); + } + }); + })); + } else { + accountViewHolder.binding.muteTimed.setChecked(false); + } + if (account.relationShip.muting) { + accountViewHolder.binding.mute.setChecked(true); + accountViewHolder.binding.mute.setOnClickListener(v -> accountsVM.unmute(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, account.id) + .observe((LifecycleOwner) context, relationShip -> { + account.relationShip = relationShip; + adapter.notifyItemChanged(position); + })); + } else { + accountViewHolder.binding.mute.setChecked(false); + accountViewHolder.binding.mute.setOnClickListener(v -> accountsVM.mute(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, account.id, null, 0) + .observe((LifecycleOwner) context, relationShip -> { + account.relationShip = relationShip; + adapter.notifyItemChanged(position); + })); + accountViewHolder.binding.muteTimed.setEnabled(true); + accountViewHolder.binding.muteTimed.setOnClickListener(v -> MastodonHelper.scheduleBoost(context, MastodonHelper.ScheduleType.TIMED_MUTED, null, account, relationShip -> { + account.relationShip = relationShip; + adapter.notifyItemChanged(position); + })); + } + + if (account.relationShip.muting_notifications) { + accountViewHolder.binding.muteNotification.setChecked(true); + accountViewHolder.binding.muteNotification.setOnClickListener(v -> accountsVM.mute(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, account.id, false, 0) + .observe((LifecycleOwner) context, relationShip -> { + account.relationShip = relationShip; + adapter.notifyItemChanged(position); + })); + } else { + accountViewHolder.binding.muteNotification.setChecked(false); + accountViewHolder.binding.muteNotification.setOnClickListener(v -> accountsVM.mute(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, account.id, true, 0) + .observe((LifecycleOwner) context, relationShip -> { + account.relationShip = relationShip; + adapter.notifyItemChanged(position); + })); + } + + if (account.relationShip.blocked_by) { + doAction = ProfileActivity.action.NOTHING; + accountViewHolder.binding.followAction.setEnabled(false); + } + + + accountViewHolder.binding.followAction.setOnClickListener(null); + accountViewHolder.binding.followAction.setOnClickListener(v -> { + if (doAction == ProfileActivity.action.NOTHING) { + Toasty.info(context, context.getString(R.string.nothing_to_do), Toast.LENGTH_LONG).show(); + } else if (doAction == ProfileActivity.action.FOLLOW) { + accountViewHolder.binding.followAction.setEnabled(false); + accountsVM.follow(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, account.id, true, false) + .observe((LifecycleOwner) context, relationShip -> { + account.relationShip = relationShip; + adapter.notifyItemChanged(position); + }); + } else if (doAction == ProfileActivity.action.UNFOLLOW) { + boolean confirm_unfollow = sharedpreferences.getBoolean(context.getString(R.string.SET_UNFOLLOW_VALIDATION), true); + if (confirm_unfollow) { + AlertDialog.Builder unfollowConfirm = new AlertDialog.Builder(context, Helper.dialogStyle()); + unfollowConfirm.setTitle(context.getString(R.string.unfollow_confirm)); + unfollowConfirm.setMessage(account.acct); + unfollowConfirm.setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss()); + unfollowConfirm.setPositiveButton(R.string.yes, (dialog, which) -> { + accountViewHolder.binding.followAction.setEnabled(false); + accountsVM.unfollow(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, account.id) + .observe((LifecycleOwner) context, relationShip -> { + account.relationShip = relationShip; + adapter.notifyItemChanged(position); + }); + dialog.dismiss(); + }); + unfollowConfirm.show(); + } else { + accountsVM.unfollow(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, account.id) + .observe((LifecycleOwner) context, relationShip -> { + account.relationShip = relationShip; + adapter.notifyItemChanged(position); + }); + } + + } + }); + } + accountViewHolder.binding.displayName.setText(account.span_display_name, TextView.BufferType.SPANNABLE); + accountViewHolder.binding.username.setText(String.format("@%s", account.acct)); + accountViewHolder.binding.bio.setText(account.span_note, TextView.BufferType.SPANNABLE); + } + + public int getCount() { + return accountList.size(); + } + + public Account getItem(int position) { + return accountList.get(position); + } + + @NonNull + @Override + public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + this.context = parent.getContext(); + DrawerAccountBinding itemBinding = DrawerAccountBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new AccountViewHolder(itemBinding); + } + + @Override + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) { + Account account = accountList.get(position); + AccountViewHolder holder = (AccountViewHolder) viewHolder; + accountManagement(context, holder, account, position, this); + + } + + public long getItemId(int position) { + return position; + } + + @Override + public int getItemCount() { + return accountList.size(); + } + + + public static class AccountViewHolder extends RecyclerView.ViewHolder { + DrawerAccountBinding binding; + + AccountViewHolder(DrawerAccountBinding itemView) { + super(itemView.getRoot()); + binding = itemView; + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/app/fedilab/android/ui/drawer/AccountListAdapter.java b/app/src/main/java/app/fedilab/android/ui/drawer/AccountListAdapter.java new file mode 100644 index 000000000..7f92a1dc0 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/ui/drawer/AccountListAdapter.java @@ -0,0 +1,139 @@ +package app.fedilab.android.ui.drawer; +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + + +import android.content.Context; +import android.content.res.ColorStateList; +import android.view.LayoutInflater; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.core.content.ContextCompat; +import androidx.lifecycle.ViewModelProvider; +import androidx.lifecycle.ViewModelStoreOwner; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.ArrayList; +import java.util.List; + +import app.fedilab.android.BaseMainActivity; +import app.fedilab.android.R; +import app.fedilab.android.client.mastodon.entities.Account; +import app.fedilab.android.client.mastodon.entities.MastodonList; +import app.fedilab.android.databinding.DrawerAccountListBinding; +import app.fedilab.android.helper.MastodonHelper; +import app.fedilab.android.viewmodel.mastodon.TimelinesVM; + + +public class AccountListAdapter extends RecyclerView.Adapter { + + private final List accountList; + private final List searchList; + private final MastodonList mastodonList; + private Context context; + private TimelinesVM timelinesVM; + + public AccountListAdapter(MastodonList mastodonList, List accountList, List searchList) { + this.mastodonList = mastodonList; + this.accountList = accountList; + this.searchList = searchList; + } + + + public int getCount() { + return searchList == null ? accountList.size() : searchList.size(); + } + + public Account getItem(int position) { + return searchList == null ? accountList.get(position) : searchList.get(position); + } + + @NonNull + @Override + public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + this.context = parent.getContext(); + timelinesVM = new ViewModelProvider((ViewModelStoreOwner) context).get(TimelinesVM.class); + DrawerAccountListBinding itemBinding = DrawerAccountListBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new AccountListViewHolder(itemBinding); + } + + @Override + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) { + Account account; + + account = getItem(position); + AccountListViewHolder holder = (AccountListViewHolder) viewHolder; + MastodonHelper.loadPPMastodon(holder.binding.avatar, account); + holder.binding.displayName.setText(account.span_display_name, TextView.BufferType.SPANNABLE); + holder.binding.username.setText(String.format("@%s", account.acct)); + + if (searchList != null) { + if (accountList.contains(account)) { + holder.binding.listAction.setBackgroundTintList(ColorStateList.valueOf(ContextCompat.getColor(context, R.color.red_1))); + holder.binding.listAction.setIconResource(R.drawable.ic_baseline_person_remove_alt_1_24); + holder.binding.listAction.setOnClickListener(v -> { + List ids = new ArrayList<>(); + ids.add(account.id); + timelinesVM.deleteAccountsList(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, mastodonList.id, ids); + accountList.remove(account); + notifyItemChanged(position); + }); + } else { + holder.binding.listAction.setBackgroundTintList(ColorStateList.valueOf(ContextCompat.getColor(context, R.color.cyanea_accent_reference))); + holder.binding.listAction.setIconResource(R.drawable.ic_baseline_person_add_alt_1_24); + holder.binding.listAction.setOnClickListener(v -> { + accountList.add(0, account); + List ids = new ArrayList<>(); + ids.add(account.id); + timelinesVM.addAccountsList(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, mastodonList.id, ids); + notifyItemChanged(position); + }); + } + } else { + holder.binding.listAction.setBackgroundTintList(ColorStateList.valueOf(ContextCompat.getColor(context, R.color.red_1))); + holder.binding.listAction.setIconResource(R.drawable.ic_baseline_person_remove_alt_1_24); + holder.binding.listAction.setOnClickListener(v -> { + accountList.remove(account); + List ids = new ArrayList<>(); + ids.add(account.id); + timelinesVM.deleteAccountsList(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, mastodonList.id, ids); + notifyItemRemoved(position); + }); + } + + } + + public long getItemId(int position) { + return position; + } + + @Override + public int getItemCount() { + return searchList == null ? accountList.size() : searchList.size(); + } + + + public static class AccountListViewHolder extends RecyclerView.ViewHolder { + DrawerAccountListBinding binding; + + AccountListViewHolder(DrawerAccountListBinding itemView) { + super(itemView.getRoot()); + binding = itemView; + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/app/fedilab/android/ui/drawer/AccountsReplyAdapter.java b/app/src/main/java/app/fedilab/android/ui/drawer/AccountsReplyAdapter.java new file mode 100644 index 000000000..12ceeea3a --- /dev/null +++ b/app/src/main/java/app/fedilab/android/ui/drawer/AccountsReplyAdapter.java @@ -0,0 +1,94 @@ +package app.fedilab.android.ui.drawer; +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + + +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.List; + +import app.fedilab.android.client.mastodon.entities.Account; +import app.fedilab.android.databinding.DrawerAccountReplyBinding; +import app.fedilab.android.helper.MastodonHelper; + + +public class AccountsReplyAdapter extends RecyclerView.Adapter { + + private final List accounts; + private final boolean[] checked; + public ActionDone actionDone; + + + public AccountsReplyAdapter(List accounts, List checked) { + this.accounts = accounts; + this.checked = new boolean[checked.size()]; + int index = 0; + for (Boolean val : checked) { + this.checked[index++] = val; + } + } + + @NonNull + @Override + public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + DrawerAccountReplyBinding itemBinding = DrawerAccountReplyBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new AccountReplyViewHolder(itemBinding); + } + + @Override + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) { + Account account = accounts.get(position); + AccountReplyViewHolder holder = (AccountReplyViewHolder) viewHolder; + holder.binding.checkbox.setOnCheckedChangeListener((compoundButton, isChecked) -> { + try { + actionDone.onContactClick(isChecked, account.acct); + checked[position] = isChecked; + } catch (Exception ignored) { + } + }); + holder.binding.checkbox.setChecked(checked[position]); + holder.binding.accountDn.setText(String.format("@%s", account.acct)); + holder.binding.accountContainer.setOnClickListener(view -> holder.binding.checkbox.performClick()); + //Profile picture + MastodonHelper.loadPPMastodon(holder.binding.accountPp, account); + } + + public long getItemId(int position) { + return position; + } + + @Override + public int getItemCount() { + return accounts.size(); + } + + + public interface ActionDone { + void onContactClick(boolean isChecked, String acct); + } + + public static class AccountReplyViewHolder extends RecyclerView.ViewHolder { + DrawerAccountReplyBinding binding; + + AccountReplyViewHolder(DrawerAccountReplyBinding itemView) { + super(itemView.getRoot()); + binding = itemView; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/fedilab/android/ui/drawer/AccountsSearchAdapter.java b/app/src/main/java/app/fedilab/android/ui/drawer/AccountsSearchAdapter.java new file mode 100644 index 000000000..221e4a365 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/ui/drawer/AccountsSearchAdapter.java @@ -0,0 +1,140 @@ +package app.fedilab.android.ui.drawer; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.Filter; +import android.widget.Filterable; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.ArrayList; +import java.util.List; + +import app.fedilab.android.client.mastodon.entities.Account; +import app.fedilab.android.databinding.DrawerAccountSearchBinding; +import app.fedilab.android.helper.MastodonHelper; + + +public class AccountsSearchAdapter extends ArrayAdapter implements Filterable { + + private final List accounts; + private final List tempAccounts; + private final List suggestions; + + private final Filter accountFilter = new Filter() { + @Override + public CharSequence convertResultToString(Object resultValue) { + Account account = (Account) resultValue; + return "@" + account.acct; + } + + @Override + protected FilterResults performFiltering(CharSequence constraint) { + if (constraint != null) { + suggestions.clear(); + suggestions.addAll(tempAccounts); + FilterResults filterResults = new FilterResults(); + filterResults.values = suggestions; + filterResults.count = suggestions.size(); + return filterResults; + } else { + return new FilterResults(); + } + } + + @Override + protected void publishResults(CharSequence constraint, FilterResults results) { + ArrayList c = (ArrayList) results.values; + if (results.count > 0) { + clear(); + addAll(c); + notifyDataSetChanged(); + } else { + clear(); + notifyDataSetChanged(); + } + } + }; + + public AccountsSearchAdapter(Context context, List accounts) { + super(context, android.R.layout.simple_list_item_1, accounts); + this.accounts = accounts; + this.tempAccounts = new ArrayList<>(accounts); + this.suggestions = new ArrayList<>(accounts); + } + + + @Override + public int getCount() { + return accounts.size(); + } + + @Override + public Account getItem(int position) { + return accounts.get(position); + } + + @Override + public long getItemId(int position) { + return position; + } + + + @NonNull + @Override + public View getView(final int position, View convertView, @NonNull ViewGroup parent) { + + final Account account = accounts.get(position); + AccountSearchViewHolder holder; + if (convertView == null) { + DrawerAccountSearchBinding drawerAccountSearchBinding = DrawerAccountSearchBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + holder = new AccountSearchViewHolder(drawerAccountSearchBinding); + holder.view = drawerAccountSearchBinding.getRoot(); + holder.view.setTag(holder); + } else { + holder = (AccountSearchViewHolder) convertView.getTag(); + } + + holder.binding.accountUn.setText(String.format("@%s", account.acct)); + holder.binding.accountDn.setText(account.display_name); + holder.binding.accountDn.setVisibility(View.VISIBLE); + MastodonHelper.loadPPMastodon(holder.binding.accountPp, account); + return holder.view; + } + + @NonNull + @Override + public Filter getFilter() { + return accountFilter; + } + + public static class AccountSearchViewHolder extends RecyclerView.ViewHolder { + DrawerAccountSearchBinding binding; + private View view; + + AccountSearchViewHolder(DrawerAccountSearchBinding itemView) { + super(itemView.getRoot()); + this.view = itemView.getRoot(); + binding = itemView; + } + } + +} diff --git a/app/src/main/java/app/fedilab/android/ui/drawer/ComposeAdapter.java b/app/src/main/java/app/fedilab/android/ui/drawer/ComposeAdapter.java new file mode 100644 index 000000000..7584824d1 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/ui/drawer/ComposeAdapter.java @@ -0,0 +1,1403 @@ +package app.fedilab.android.ui.drawer; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + + +import static app.fedilab.android.BaseMainActivity.instanceInfo; +import static app.fedilab.android.activities.ComposeActivity.MY_PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE; + +import android.Manifest; +import android.annotation.SuppressLint; +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.content.res.ColorStateList; +import android.graphics.Bitmap; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.Build; +import android.os.Handler; +import android.os.Looper; +import android.text.Editable; +import android.text.Html; +import android.text.InputFilter; +import android.text.SpannableString; +import android.text.TextWatcher; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.GridView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.widget.AppCompatEditText; +import androidx.appcompat.widget.LinearLayoutCompat; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; +import androidx.core.widget.ImageViewCompat; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.ViewModelProvider; +import androidx.lifecycle.ViewModelStoreOwner; +import androidx.preference.PreferenceManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.request.RequestOptions; +import com.bumptech.glide.request.target.CustomTarget; +import com.bumptech.glide.request.transition.Transition; + +import java.io.File; +import java.text.Normalizer; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import app.fedilab.android.BaseMainActivity; +import app.fedilab.android.R; +import app.fedilab.android.activities.ComposeActivity; +import app.fedilab.android.client.entities.Account; +import app.fedilab.android.client.entities.StatusDraft; +import app.fedilab.android.client.mastodon.entities.Attachment; +import app.fedilab.android.client.mastodon.entities.Emoji; +import app.fedilab.android.client.mastodon.entities.EmojiInstance; +import app.fedilab.android.client.mastodon.entities.Mention; +import app.fedilab.android.client.mastodon.entities.Poll; +import app.fedilab.android.client.mastodon.entities.Status; +import app.fedilab.android.client.mastodon.entities.Tag; +import app.fedilab.android.databinding.ComposeAttachmentItemBinding; +import app.fedilab.android.databinding.ComposePollBinding; +import app.fedilab.android.databinding.ComposePollItemBinding; +import app.fedilab.android.databinding.DrawerStatusComposeBinding; +import app.fedilab.android.databinding.DrawerStatusSimpleBinding; +import app.fedilab.android.databinding.PopupMediaDescriptionBinding; +import app.fedilab.android.exception.DBException; +import app.fedilab.android.helper.Helper; +import app.fedilab.android.helper.MastodonHelper; +import app.fedilab.android.viewmodel.mastodon.AccountsVM; +import app.fedilab.android.viewmodel.mastodon.SearchVM; +import es.dmoral.toasty.Toasty; + + +public class ComposeAdapter extends RecyclerView.Adapter { + private static final int searchDeep = 15; + public static boolean autocomplete = false; + public static String[] ALPHA = {"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", + "s", "t", "u", "v", "w", "x", "y", "z", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "!", ",", "?", + ".", "'"}; + public static String[] MORSE = {".-", "-...", "-.-.", "-..", ".", "..-.", "--.", "....", "..", ".---", "-.-", ".-..", + "--", "-.", "---", ".--.", "--.-", ".-.", "...", "-", "..-", "...-", ".--", "-..-", "-.--", "--..", ".----", + "..---", "...--", "....-", ".....", "-....", "--...", "---..", "----.", "-----", "-.-.--", "--..--", + "..--..", ".-.-.-", ".----.",}; + private final List statusList; + private final int TYPE_NORMAL = 0; + private static final int TYPE_COMPOSE = 1; + private final Account account; + public ManageDrafts manageDrafts; + List emojis; + private int statusCount; + private Context context; + private AlertDialog alertDialogEmoji; + + public ComposeAdapter(List statusList, int statusCount, Account account) { + this.statusList = statusList; + this.statusCount = statusCount; + this.account = account; + } + + private static void updateCharacterCount(ComposeViewHolder composeViewHolder) { + int charCount = MastodonHelper.countLength(composeViewHolder); + composeViewHolder.binding.characterCount.setText(String.valueOf(charCount)); + composeViewHolder.binding.characterProgress.setProgress(charCount); + } + + + //Create text when mentioning a toot + public void loadMentions(Status status) { + //Get the first draft + statusList.get(statusCount).text = String.format("\n\nvia @%s\n\n%s\n\n", status.account.acct, status.url); + notifyItemChanged(statusCount); + } + + /** + * Manage mentions displayed when replying to a message + * + * @param context Context + * @param statusDraft {@link Status} - Status that user is replying + * @param holder {@link ComposeViewHolder} - current compose viewHolder + */ + private void manageMentions(Context context, Status statusDraft, ComposeViewHolder holder) { + + if (statusDraft.mentions != null && (statusDraft.text == null || statusDraft.text.length() == 0)) { + //Retrieves mentioned accounts + OP and adds them at the beginin of the toot + final SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(context); + Mention inReplyToUser = null; + for (Mention mention : statusDraft.mentions) { + //Mentioned account has a null id + if (mention.id == null) { + inReplyToUser = mention; + break; + } + } + //Put other accounts mentioned at the bottom + boolean capitalize = sharedpreferences.getBoolean(context.getString(R.string.SET_CAPITALIZE), true); + if (inReplyToUser != null) { + if (capitalize) { + statusDraft.text = inReplyToUser.acct + "\n\n"; + } else { + statusDraft.text = inReplyToUser.acct + " "; + } + } + holder.binding.content.setText(statusDraft.text); + statusDraft.cursorPosition = statusDraft.text.length(); + if (statusDraft.mentions.size() > 1) { + statusDraft.text += "\n\n"; + for (Mention mention : statusDraft.mentions) { + if (mention.id != null && mention.acct != null && !mention.id.equals(BaseMainActivity.client_id)) { + String tootTemp = String.format("@%s ", mention.acct); + statusDraft.text = String.format("%s ", (statusDraft.text + tootTemp.trim())); + } + } + } + holder.binding.content.setText(statusDraft.text); + updateCharacterCount(holder); + holder.binding.content.requestFocus(); + holder.binding.content.post(() -> { + holder.binding.content.setSelection(statusDraft.cursorPosition); //Put cursor at the end + buttonVisibility(holder); + }); + } else { + holder.binding.content.requestFocus(); + } + } + + + public void setStatusCount(int count) { + statusCount = count; + } + + public int getCount() { + return (statusList.size()); + } + + public Status getItem(int position) { + return statusList.get(position); + } + + @Override + public int getItemViewType(int position) { + return position >= statusCount ? TYPE_COMPOSE : TYPE_NORMAL; + } + + @NonNull + @Override + public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + context = parent.getContext(); + if (viewType == TYPE_NORMAL) { + DrawerStatusSimpleBinding itemBinding = DrawerStatusSimpleBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new StatusSimpleViewHolder(itemBinding); + } else { + DrawerStatusComposeBinding itemBinding = DrawerStatusComposeBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new ComposeViewHolder(itemBinding); + } + } + + private void pickupMedia(ComposeActivity.mediaType type, int position) { + if (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE) != + PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions((Activity) context, + new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, + MY_PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE); + return; + } + Intent intent; + intent = new Intent(Intent.ACTION_GET_CONTENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setType("*/*"); + intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); + String[] mimetypes = new String[0]; + if (type == ComposeActivity.mediaType.PHOTO) { + if (instanceInfo.getMimeTypeImage().size() > 0) { + mimetypes = instanceInfo.getMimeTypeImage().toArray(new String[0]); + } else { + mimetypes = new String[]{"image/*"}; + } + } else if (type == ComposeActivity.mediaType.VIDEO) { + if (instanceInfo.getMimeTypeVideo().size() > 0) { + mimetypes = instanceInfo.getMimeTypeVideo().toArray(new String[0]); + } else { + mimetypes = new String[]{"video/*"}; + } + } else if (type == ComposeActivity.mediaType.AUDIO) { + if (instanceInfo.getMimeTypeAudio().size() > 0) { + mimetypes = instanceInfo.getMimeTypeAudio().toArray(new String[0]); + } else { + mimetypes = new String[]{"audio/mpeg", "audio/opus", "audio/flac", "audio/wav", "audio/ogg"}; + } + } else if (type == ComposeActivity.mediaType.ALL) { + if (instanceInfo.getMimeTypeOther().size() > 0) { + mimetypes = instanceInfo.getMimeTypeOther().toArray(new String[0]); + } else { + mimetypes = new String[]{"*/*"}; + } + } + intent.putExtra(Intent.EXTRA_MIME_TYPES, mimetypes); + ((Activity) context).startActivityForResult(intent, (ComposeActivity.PICK_MEDIA + position)); + } + + /** + * Manage the visibility of the button (+/-) for adding a message to the composed thread + * + * @param holder - ComposeViewHolder + */ + private void buttonVisibility(ComposeViewHolder holder) { + //First message - Needs at least one char to display the + button + if (holder.getLayoutPosition() == statusCount && canBeRemoved(statusList.get(holder.getLayoutPosition()))) { + holder.binding.addRemoveStatus.setVisibility(View.GONE); + return; + } + + //Manage last compose drawer button visibility + if (holder.getLayoutPosition() == (getItemCount() - 1)) { + if (statusList.size() > statusCount + 1) { + if (canBeRemoved(statusList.get(statusList.size() - 1))) { + holder.binding.addRemoveStatus.setImageResource(R.drawable.ic_compose_thread_remove_status); + holder.binding.addRemoveStatus.setContentDescription(context.getString(R.string.remove_status)); + holder.binding.addRemoveStatus.setOnClickListener(v -> { + manageDrafts.onItemDraftDeleted(statusList.get(holder.getLayoutPosition()), holder.getLayoutPosition()); + notifyItemChanged((getItemCount() - 1)); + }); + } else { + holder.binding.addRemoveStatus.setImageResource(R.drawable.ic_compose_thread_add_status); + holder.binding.addRemoveStatus.setContentDescription(context.getString(R.string.add_status)); + holder.binding.addRemoveStatus.setOnClickListener(v -> { + manageDrafts.onItemDraftAdded(holder.getLayoutPosition()); + buttonVisibility(holder); + }); + } + } else { + holder.binding.addRemoveStatus.setImageResource(R.drawable.ic_compose_thread_add_status); + holder.binding.addRemoveStatus.setContentDescription(context.getString(R.string.add_status)); + holder.binding.addRemoveStatus.setOnClickListener(v -> { + manageDrafts.onItemDraftAdded(holder.getLayoutPosition()); + buttonVisibility(holder); + }); + } + holder.binding.addRemoveStatus.setVisibility(View.VISIBLE); + holder.binding.buttonPost.setVisibility(View.VISIBLE); + } else { + holder.binding.addRemoveStatus.setVisibility(View.GONE); + holder.binding.buttonPost.setVisibility(View.GONE); + } + + } + + /** + * Check content of the draft to set if it can be removed (empty poll / media / text / spoiler) + * + * @param draft - Status + * @return boolean + */ + private boolean canBeRemoved(Status draft) { + return draft.poll == null + && (draft.media_attachments == null || draft.media_attachments.size() == 0) + && (draft.text == null || draft.text.trim().length() == 0) + && (draft.spoiler_text == null || draft.spoiler_text.trim().length() == 0); + } + + /** + * Add an attachment from ComposeActivity + * + * @param position int - position of the drawer that added a media + * @param uris List - uris of the media + */ + public void addAttachment(int position, List uris) { + + if (position == -1) { + position = statusList.size() - 1; + } + if (statusList.get(position).media_attachments == null) { + statusList.get(position).media_attachments = new ArrayList<>(); + } + int finalPosition = position; + Helper.createAttachmentFromUri(context, uris, attachment -> { + statusList.get(finalPosition).media_attachments.add(attachment); + notifyItemChanged(finalPosition); + }); + } + + //<------ Manage contact from compose activity + //It only targets last message in a thread + //Return content of last compose message + public String getLastComposeContent() { + return statusList.get(statusList.size() - 1).text != null ? statusList.get(statusList.size() - 1).text : ""; + } + + //Used to write contact when composing + public void updateContent(boolean checked, String acct) { + if (checked) { + if (!statusList.get(statusList.size() - 1).text.contains(acct)) + statusList.get(statusList.size() - 1).text = String.format("%s %s", acct, statusList.get(statusList.size() - 1).text); + } else { + statusList.get(statusList.size() - 1).text = statusList.get(statusList.size() - 1).text.replaceAll("\\s*" + acct, ""); + } + notifyItemChanged(statusList.size() - 1); + } + + //Put cursor to the end after changing contacts + public void putCursor() { + statusList.get(statusList.size() - 1).setCursorToEnd = true; + notifyItemChanged(statusList.size() - 1); + } + //------- end contact -----> + + private void displayAttachments(ComposeViewHolder holder, int position, int scrollToMediaPosition) { + if (statusList.size() > position && statusList.get(position).media_attachments != null) { + holder.binding.attachmentsList.removeAllViews(); + List attachmentList = statusList.get(position).media_attachments; + if (attachmentList != null && attachmentList.size() > 0) { + holder.binding.sensitiveMedia.setVisibility(View.VISIBLE); + holder.binding.sensitiveMedia.setChecked(BaseMainActivity.accountWeakReference.get().mastodon_account.source.sensitive); + statusList.get(position).sensitive = BaseMainActivity.accountWeakReference.get().mastodon_account.source.sensitive; + holder.binding.sensitiveMedia.setOnCheckedChangeListener((buttonView, isChecked) -> statusList.get(position).sensitive = isChecked); + int mediaPosition = 0; + for (Attachment attachment : attachmentList) { + ComposeAttachmentItemBinding composeAttachmentItemBinding = ComposeAttachmentItemBinding.inflate(LayoutInflater.from(context), holder.binding.attachmentsList, false); + composeAttachmentItemBinding.buttonPlay.setVisibility(View.GONE); + String attachmentPath = attachment.local_path != null && !attachment.local_path.trim().isEmpty() ? attachment.local_path : attachment.preview_url; + if (attachment.type != null || attachment.mimeType != null) { + if ((attachment.type != null && attachment.type.toLowerCase().startsWith("image")) || (attachment.mimeType != null && attachment.mimeType.toLowerCase().startsWith("image"))) { + Glide.with(composeAttachmentItemBinding.preview.getContext()) + .load(attachmentPath) + .into(composeAttachmentItemBinding.preview); + } else if ((attachment.type != null && attachment.type.toLowerCase().startsWith("video")) || (attachment.mimeType != null && attachment.mimeType.toLowerCase().startsWith("video"))) { + composeAttachmentItemBinding.buttonPlay.setVisibility(View.VISIBLE); + long interval = 2000; + RequestOptions options = new RequestOptions().frame(interval); + Glide.with(composeAttachmentItemBinding.preview.getContext()).asBitmap() + .load(attachmentPath) + .apply(options) + .into(composeAttachmentItemBinding.preview); + } else if ((attachment.type != null && attachment.type.toLowerCase().startsWith("audio")) || (attachment.mimeType != null && attachment.mimeType.toLowerCase().startsWith("audio"))) { + Glide.with(composeAttachmentItemBinding.preview.getContext()) + .load(R.drawable.ic_baseline_audio_file_24) + .into(composeAttachmentItemBinding.preview); + } else { + Glide.with(composeAttachmentItemBinding.preview.getContext()) + .load(R.drawable.ic_baseline_insert_drive_file_24) + .into(composeAttachmentItemBinding.preview); + } + } else { + Glide.with(composeAttachmentItemBinding.preview.getContext()) + .load(R.drawable.ic_baseline_insert_drive_file_24) + .into(composeAttachmentItemBinding.preview); + } + if (mediaPosition == 0) { + composeAttachmentItemBinding.buttonOrderUp.setVisibility(View.INVISIBLE); + } else { + composeAttachmentItemBinding.buttonOrderUp.setVisibility(View.VISIBLE); + } + if (mediaPosition == attachmentList.size() - 1) { + composeAttachmentItemBinding.buttonOrderDown.setVisibility(View.INVISIBLE); + } else { + composeAttachmentItemBinding.buttonOrderDown.setVisibility(View.VISIBLE); + } + //Remote attachments when deleting/redrafting can't be ordered + if (attachment.local_path == null) { + composeAttachmentItemBinding.buttonOrderUp.setVisibility(View.INVISIBLE); + composeAttachmentItemBinding.buttonOrderDown.setVisibility(View.INVISIBLE); + } + int finalMediaPosition = mediaPosition; + + composeAttachmentItemBinding.buttonDescription.setOnClickListener(v -> { + AlertDialog.Builder builderInner = new AlertDialog.Builder(context, Helper.dialogStyle()); + builderInner.setTitle(R.string.upload_form_description); + PopupMediaDescriptionBinding popupMediaDescriptionBinding = PopupMediaDescriptionBinding.inflate(LayoutInflater.from(context), null, false); + builderInner.setView(popupMediaDescriptionBinding.getRoot()); + + popupMediaDescriptionBinding.mediaDescription.setFilters(new InputFilter[]{new InputFilter.LengthFilter(1500)}); + Glide.with(popupMediaDescriptionBinding.mediaPicture.getContext()) + .asBitmap() + .load(attachmentPath) + .into(new CustomTarget() { + @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN) + @Override + public void onResourceReady(@NonNull Bitmap resource, Transition transition) { + popupMediaDescriptionBinding.mediaPicture.setImageBitmap(resource); + popupMediaDescriptionBinding.mediaPicture.setImageAlpha(60); + } + + @Override + public void onLoadCleared(@Nullable Drawable placeholder) { + + } + }); + builderInner.setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss()); + if (attachment.description != null) { + popupMediaDescriptionBinding.mediaDescription.setText(attachment.description); + popupMediaDescriptionBinding.mediaDescription.setSelection(popupMediaDescriptionBinding.mediaDescription.getText().length()); + } + builderInner.setPositiveButton(R.string.validate, (dialog, which) -> { + attachment.description = popupMediaDescriptionBinding.mediaDescription.getText().toString(); + displayAttachments(holder, position, finalMediaPosition); + dialog.dismiss(); + }); + AlertDialog alertDialog = builderInner.create(); + Objects.requireNonNull(alertDialog.getWindow()).setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE); + alertDialog.show(); + popupMediaDescriptionBinding.mediaDescription.requestFocus(); + + }); + + composeAttachmentItemBinding.buttonOrderUp.setOnClickListener(v -> { + if (finalMediaPosition > 0 && attachmentList.size() > 1) { + Attachment at1 = attachmentList.get(finalMediaPosition); + Attachment at2 = attachmentList.get(finalMediaPosition - 1); + attachmentList.set(finalMediaPosition - 1, at1); + attachmentList.set(finalMediaPosition, at2); + displayAttachments(holder, position, finalMediaPosition - 1); + } + }); + composeAttachmentItemBinding.buttonOrderDown.setOnClickListener(v -> { + if (finalMediaPosition < (attachmentList.size() - 1) && attachmentList.size() > 1) { + Attachment at1 = attachmentList.get(finalMediaPosition); + Attachment at2 = attachmentList.get(finalMediaPosition + 1); + attachmentList.set(finalMediaPosition, at2); + attachmentList.set(finalMediaPosition + 1, at1); + displayAttachments(holder, position, finalMediaPosition + 1); + } + }); + composeAttachmentItemBinding.buttonRemove.setOnClickListener(v -> { + AlertDialog.Builder builderInner = new AlertDialog.Builder(context, Helper.dialogStyle()); + builderInner.setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss()); + builderInner.setPositiveButton(R.string.delete, (dialog, which) -> { + attachmentList.remove(attachment); + displayAttachments(holder, position, finalMediaPosition); + new Thread(() -> { + if (attachment.local_path != null) { + File fileToDelete = new File(attachment.local_path); + if (fileToDelete.exists()) { + //noinspection ResultOfMethodCallIgnored + fileToDelete.delete(); + } + } + }).start(); + + }); + builderInner.setMessage(R.string.toot_delete_media); + builderInner.show(); + }); + composeAttachmentItemBinding.preview.setOnClickListener(v -> displayAttachments(holder, position, finalMediaPosition)); + if (attachment.description == null || attachment.description.trim().isEmpty()) { + ImageViewCompat.setImageTintList(composeAttachmentItemBinding.buttonDescription, ColorStateList.valueOf(ContextCompat.getColor(context, R.color.errorColor))); + } else { + ImageViewCompat.setImageTintList(composeAttachmentItemBinding.buttonDescription, ColorStateList.valueOf(ContextCompat.getColor(context, R.color.successColor))); + } + holder.binding.attachmentsList.addView(composeAttachmentItemBinding.getRoot()); + mediaPosition++; + } + holder.binding.attachmentsList.setVisibility(View.VISIBLE); + if (scrollToMediaPosition >= 0 && holder.binding.attachmentsList.getChildCount() < scrollToMediaPosition) { + holder.binding.attachmentsList.requestChildFocus(holder.binding.attachmentsList.getChildAt(scrollToMediaPosition), holder.binding.attachmentsList.getChildAt(scrollToMediaPosition)); + } + } else { + holder.binding.attachmentsList.setVisibility(View.GONE); + holder.binding.sensitiveMedia.setVisibility(View.GONE); + } + } else { + holder.binding.attachmentsList.setVisibility(View.GONE); + holder.binding.sensitiveMedia.setVisibility(View.GONE); + } + buttonState(holder); + } + + /** + * Manage state of media and poll button + * + * @param holder ComposeViewHolder + */ + private void buttonState(ComposeViewHolder holder) { + if (BaseMainActivity.software == null || BaseMainActivity.software.toUpperCase().compareTo("MASTODON") == 0) { + Status statusDraft = statusList.get(holder.getAdapterPosition()); + if (statusDraft.poll == null) { + holder.binding.buttonAttachImage.setEnabled(true); + holder.binding.buttonAttachVideo.setEnabled(true); + holder.binding.buttonAttachAudio.setEnabled(true); + holder.binding.buttonAttachManual.setEnabled(true); + } else { + holder.binding.buttonAttachImage.setEnabled(false); + holder.binding.buttonAttachVideo.setEnabled(false); + holder.binding.buttonAttachAudio.setEnabled(false); + holder.binding.buttonAttachManual.setEnabled(false); + holder.binding.buttonPoll.setEnabled(true); + } + holder.binding.buttonPoll.setEnabled(statusDraft.media_attachments == null || statusDraft.media_attachments.size() <= 0); + } + } + + public long getItemId(int position) { + return position; + } + + @Override + public int getItemCount() { + return statusList.size(); + } + + + /** + * Initialize text watcher for content writing + * It will allow to complete autocomplete edit text while starting words with @, #, : etc. + * + * @param holder {@link ComposeViewHolder} - current compose viewHolder + * @return {@link TextWatcher} + */ + public TextWatcher initializeTextWatcher(ComposeAdapter.ComposeViewHolder holder) { + final List[] emojis = new List[]{null}; + String pattern = "(.|\\s)*(@[\\w_-]+@[a-z0-9.\\-]+|@[\\w_-]+)"; + final Pattern mentionPattern = Pattern.compile(pattern); + + String patternTag = "^(.|\\s)*(#([\\w-]{2,}))$"; + final Pattern tagPattern = Pattern.compile(patternTag); + + String patternEmoji = "^(.|\\s)*(:([\\w_]+))$"; + final Pattern emojiPattern = Pattern.compile(patternEmoji); + final int[] currentCursorPosition = {holder.binding.content.getSelectionStart()}; + final String[] newContent = {null}; + final int[] searchLength = {searchDeep}; + TextWatcher textw; + AccountsVM accountsVM = new ViewModelProvider((ViewModelStoreOwner) context).get(AccountsVM.class); + SearchVM searchVM = new ViewModelProvider((ViewModelStoreOwner) context).get(SearchVM.class); + textw = new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + + } + + @Override + public void afterTextChanged(Editable s) { + statusList.get(holder.getAdapterPosition()).text = s.toString(); + if (s.toString().trim().length() < 2) { + buttonVisibility(holder); + } + //Update cursor position + statusList.get(holder.getAdapterPosition()).cursorPosition = holder.binding.content.getSelectionStart(); + if (autocomplete) { + holder.binding.content.removeTextChangedListener(this); + Thread thread = new Thread() { + @Override + public void run() { + String fedilabHugsTrigger = ":fedilab_hugs:"; + String fedilabMorseTrigger = ":fedilab_morse:"; + + if (s.toString().contains(fedilabHugsTrigger)) { + newContent[0] = s.toString().replaceAll(fedilabHugsTrigger, ""); + int currentLength = MastodonHelper.countLength(holder); + int toFill = 500 - currentLength; + if (toFill <= 0) { + return; + } + StringBuilder hugs = new StringBuilder(); + for (int i = 0; i < toFill; i++) { + hugs.append(new String(Character.toChars(0x1F917))); + } + + Handler mainHandler = new Handler(Looper.getMainLooper()); + + Runnable myRunnable = () -> { + newContent[0] = newContent[0] + hugs; + holder.binding.content.setText(newContent[0]); + holder.binding.content.setSelection(holder.binding.content.getText().length()); + autocomplete = false; + updateCharacterCount(holder); + }; + mainHandler.post(myRunnable); + } else if (s.toString().contains(fedilabMorseTrigger)) { + newContent[0] = s.toString().replaceAll(fedilabMorseTrigger, "").trim(); + List mentions = new ArrayList<>(); + String mentionPattern = "@[a-z0-9_]+(@[a-z0-9.\\-]+[a-z0-9]+)?"; + final Pattern mPattern = Pattern.compile(mentionPattern, Pattern.CASE_INSENSITIVE); + Matcher matcherMentions = mPattern.matcher(newContent[0]); + while (matcherMentions.find()) { + mentions.add(matcherMentions.group()); + } + for (String mention : mentions) { + newContent[0] = newContent[0].replace(mention, ""); + } + newContent[0] = Normalizer.normalize(newContent[0], Normalizer.Form.NFD); + newContent[0] = newContent[0].replaceAll("[^\\p{ASCII}]", ""); + + HashMap ALPHA_TO_MORSE = new HashMap<>(); + for (int i = 0; i < ALPHA.length && i < MORSE.length; i++) { + ALPHA_TO_MORSE.put(ALPHA[i], MORSE[i]); + } + StringBuilder builder = new StringBuilder(); + String[] words = newContent[0].trim().split(" "); + + for (String word : words) { + for (int i = 0; i < word.length(); i++) { + String morse = ALPHA_TO_MORSE.get(word.substring(i, i + 1).toLowerCase()); + builder.append(morse).append(" "); + } + + builder.append(" "); + } + newContent[0] = ""; + for (String mention : mentions) { + newContent[0] += mention + " "; + } + newContent[0] += builder.toString(); + + Handler mainHandler = new Handler(Looper.getMainLooper()); + + Runnable myRunnable = () -> { + holder.binding.content.setText(newContent[0]); + holder.binding.content.setSelection(holder.binding.content.getText().length()); + autocomplete = false; + updateCharacterCount(holder); + }; + mainHandler.post(myRunnable); + } + } + }; + thread.start(); + return; + } + + if (holder.binding.content.getSelectionStart() != 0) + currentCursorPosition[0] = holder.binding.content.getSelectionStart(); + if (s.toString().length() == 0) + currentCursorPosition[0] = 0; + //Only check last 15 characters before cursor position to avoid lags + //Less than 15 characters are written before the cursor position + searchLength[0] = Math.min(currentCursorPosition[0], searchDeep); + + + if (currentCursorPosition[0] - (searchLength[0] - 1) < 0 || currentCursorPosition[0] == 0 || currentCursorPosition[0] > s.toString().length()) + return; + + String patternh = "^(.|\\s)*(:fedilab_hugs:)$"; + final Pattern hPattern = Pattern.compile(patternh); + Matcher mh = hPattern.matcher((s.toString().substring(currentCursorPosition[0] - searchLength[0], currentCursorPosition[0]))); + + if (mh.matches()) { + autocomplete = true; + return; + } + + String patternM = "^(.|\\s)*(:fedilab_morse:)$"; + final Pattern mPattern = Pattern.compile(patternM); + Matcher mm = mPattern.matcher((s.toString().substring(currentCursorPosition[0] - searchLength[0], currentCursorPosition[0]))); + if (mm.matches()) { + autocomplete = true; + return; + } + String[] searchInArray = (s.toString().substring(currentCursorPosition[0] - searchLength[0], currentCursorPosition[0])).split("\\s"); + if (searchInArray.length < 1) { + return; + } + String searchIn = searchInArray[searchInArray.length - 1]; + Matcher matcherMention, matcherTag, matcherEmoji; + matcherMention = mentionPattern.matcher(searchIn); + matcherTag = tagPattern.matcher(searchIn); + matcherEmoji = emojiPattern.matcher(searchIn); + if (matcherMention.matches()) { + String searchGroup = matcherMention.group(); + accountsVM.searchAccounts(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, searchGroup, 10, true, false).observe((LifecycleOwner) context, accounts -> { + if (accounts == null) { + return; + } + int currentCursorPosition = holder.binding.content.getSelectionStart(); + AccountsSearchAdapter accountsListAdapter = new AccountsSearchAdapter(context, accounts); + holder.binding.content.setThreshold(1); + holder.binding.content.setAdapter(accountsListAdapter); + final String oldContent = holder.binding.content.getText().toString(); + if (oldContent.length() >= currentCursorPosition) { + String[] searchA = oldContent.substring(0, currentCursorPosition).split("@"); + if (searchA.length > 0) { + final String search = searchA[searchA.length - 1]; + holder.binding.content.setOnItemClickListener((parent, view, position, id) -> { + app.fedilab.android.client.mastodon.entities.Account account = accounts.get(position); + String deltaSearch = ""; + int searchLength = searchDeep; + if (currentCursorPosition < searchDeep) { //Less than 15 characters are written before the cursor position + searchLength = currentCursorPosition; + } + if (currentCursorPosition - searchLength > 0 && currentCursorPosition < oldContent.length()) + deltaSearch = oldContent.substring(currentCursorPosition - searchLength, currentCursorPosition); + else { + if (currentCursorPosition >= oldContent.length()) + deltaSearch = oldContent.substring(currentCursorPosition - searchLength); + } + + if (!search.equals("")) + deltaSearch = deltaSearch.replace("@" + search, ""); + String newContent = oldContent.substring(0, currentCursorPosition - searchLength); + newContent += deltaSearch; + newContent += "@" + account.acct + " "; + int newPosition = newContent.length(); + if (currentCursorPosition < oldContent.length()) + newContent += oldContent.substring(currentCursorPosition); + holder.binding.content.setText(newContent); + updateCharacterCount(holder); + holder.binding.content.setSelection(newPosition); + AccountsSearchAdapter accountsListAdapter1 = new AccountsSearchAdapter(context, new ArrayList<>()); + holder.binding.content.setThreshold(1); + holder.binding.content.setAdapter(accountsListAdapter1); + }); + } + } + + }); + } else if (matcherTag.matches()) { + String searchGroup = matcherTag.group(3); + if (searchGroup != null) { + searchVM.search(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, searchGroup, null, + "hashtags", false, true, false, 0, + null, null, 10).observe((LifecycleOwner) context, + results -> { + if (results == null) { + return; + } + int currentCursorPosition = holder.binding.content.getSelectionStart(); + TagsSearchAdapter tagsSearchAdapter = new TagsSearchAdapter(context, results.hashtags); + holder.binding.content.setThreshold(1); + holder.binding.content.setAdapter(tagsSearchAdapter); + final String oldContent = holder.binding.content.getText().toString(); + if (oldContent.length() < currentCursorPosition) + return; + String[] searchA = oldContent.substring(0, currentCursorPosition).split("#"); + if (searchA.length < 1) + return; + final String search = searchA[searchA.length - 1]; + holder.binding.content.setOnItemClickListener((parent, view, position, id) -> { + if (position >= results.hashtags.size()) + return; + Tag tag = results.hashtags.get(position); + String deltaSearch = ""; + int searchLength = searchDeep; + if (currentCursorPosition < searchDeep) { //Less than 15 characters are written before the cursor position + searchLength = currentCursorPosition; + } + if (currentCursorPosition - searchLength > 0 && currentCursorPosition < oldContent.length()) + deltaSearch = oldContent.substring(currentCursorPosition - searchLength, currentCursorPosition); + else { + if (currentCursorPosition >= oldContent.length()) + deltaSearch = oldContent.substring(currentCursorPosition - searchLength); + } + + if (!search.equals("")) + deltaSearch = deltaSearch.replace("#" + search, ""); + String newContent = oldContent.substring(0, currentCursorPosition - searchLength); + newContent += deltaSearch; + newContent += "#" + tag.name + " "; + int newPosition = newContent.length(); + if (currentCursorPosition < oldContent.length()) + newContent += oldContent.substring(currentCursorPosition); + holder.binding.content.setText(newContent); + updateCharacterCount(holder); + holder.binding.content.setSelection(newPosition); + TagsSearchAdapter tagsSearchAdapter1 = new TagsSearchAdapter(context, new ArrayList<>()); + holder.binding.content.setThreshold(1); + holder.binding.content.setAdapter(tagsSearchAdapter1); + }); + }); + } + } else if (matcherEmoji.matches()) { + String shortcode = matcherEmoji.group(3); + new Thread(() -> { + List emojisToDisplay = new ArrayList<>(); + try { + if (emojis[0] == null) { + emojis[0] = new EmojiInstance(context).getEmojiList(BaseMainActivity.currentInstance); + } + if (emojis[0] == null) { + return; + } + for (Emoji emoji : emojis[0]) { + if (shortcode != null && emoji.shortcode.contains(shortcode)) { + emojisToDisplay.add(emoji); + if (emojisToDisplay.size() >= 10) { + break; + } + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + Runnable myRunnable = () -> { + int currentCursorPosition = holder.binding.content.getSelectionStart(); + EmojiSearchAdapter emojisSearchAdapter = new EmojiSearchAdapter(context, emojis[0]); + holder.binding.content.setThreshold(1); + holder.binding.content.setAdapter(emojisSearchAdapter); + final String oldContent = holder.binding.content.getText().toString(); + String[] searchA = oldContent.substring(0, currentCursorPosition).split(":"); + if (searchA.length > 0) { + final String search = searchA[searchA.length - 1]; + holder.binding.content.setOnItemClickListener((parent, view, position, id) -> { + String shortcodeSelected = emojis[0].get(position).shortcode; + String deltaSearch = ""; + int searchLength = searchDeep; + if (currentCursorPosition < searchDeep) { //Less than 15 characters are written before the cursor position + searchLength = currentCursorPosition; + } + if (currentCursorPosition - searchLength > 0 && currentCursorPosition < oldContent.length()) + deltaSearch = oldContent.substring(currentCursorPosition - searchLength, currentCursorPosition); + else { + if (currentCursorPosition >= oldContent.length()) + deltaSearch = oldContent.substring(currentCursorPosition - searchLength); + } + + if (!search.equals("")) + deltaSearch = deltaSearch.replace(":" + search, ""); + String newContent = oldContent.substring(0, currentCursorPosition - searchLength); + newContent += deltaSearch; + newContent += ":" + shortcodeSelected + ": "; + int newPosition = newContent.length(); + if (currentCursorPosition < oldContent.length()) + newContent += oldContent.substring(currentCursorPosition); + holder.binding.content.setText(newContent); + updateCharacterCount(holder); + holder.binding.content.setSelection(newPosition); + EmojiSearchAdapter emojisSearchAdapter1 = new EmojiSearchAdapter(context, new ArrayList<>()); + holder.binding.content.setThreshold(1); + holder.binding.content.setAdapter(emojisSearchAdapter1); + }); + } + }; + mainHandler.post(myRunnable); + } catch (DBException e) { + e.printStackTrace(); + } + }).start(); + + } else { + holder.binding.content.dismissDropDown(); + } + + updateCharacterCount(holder); + } + }; + return textw; + } + + public static StatusDraft prepareDraft(List statusList, ComposeAdapter composeAdapter, String instance, String user_id) { + //Collect all statusCompose + List statusDrafts = new ArrayList<>(); + List statusReplies = new ArrayList<>(); + int i = 0; + for (Status status : statusList) { + + //Statuses must be sent + if (composeAdapter.getItemViewType(i) == TYPE_COMPOSE) { + statusDrafts.add(status); + } else { + statusReplies.add(status); + } + i++; + } + StatusDraft statusDraftDB = new StatusDraft(); + statusDraftDB.statusReplyList = statusReplies; + statusDraftDB.statusDraftList = statusDrafts; + statusDraftDB.instance = instance; + statusDraftDB.user_id = user_id; + return statusDraftDB; + } + + @SuppressLint("ClickableViewAccessibility") + @Override + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) { + if (getItemViewType(position) == TYPE_NORMAL) { + Status status = statusList.get(position); + StatusSimpleViewHolder holder = (StatusSimpleViewHolder) viewHolder; + holder.binding.statusContent.setText(status.span_content, TextView.BufferType.SPANNABLE); + MastodonHelper.loadPPMastodon(holder.binding.avatar, status.account); + holder.binding.displayName.setText(status.account.span_display_name, TextView.BufferType.SPANNABLE); + holder.binding.username.setText(String.format("@%s", status.account.acct)); + if (status.spoiler_text != null && !status.spoiler_text.trim().isEmpty()) { + + holder.binding.spoiler.setVisibility(View.VISIBLE); + holder.binding.spoiler.setText(status.span_spoiler_text, TextView.BufferType.SPANNABLE); + } else { + holder.binding.spoiler.setVisibility(View.GONE); + holder.binding.spoiler.setText(null); + } + } else if (getItemViewType(position) == TYPE_COMPOSE) { + Status statusDraft = statusList.get(position); + //Fill emoji and instance info + if (emojis == null) { + new Thread(() -> { + try { + emojis = new EmojiInstance(context).getEmojiList(BaseMainActivity.currentInstance); + } catch (DBException e) { + e.printStackTrace(); + } + }).start(); + } + + ComposeViewHolder holder = (ComposeViewHolder) viewHolder; + holder.binding.buttonAttach.setOnClickListener(v -> { + if (instanceInfo.configuration.media_attachments.supported_mime_types != null) { + if (instanceInfo.getMimeTypeAudio().size() == 0) { + holder.binding.buttonAttachAudio.setEnabled(false); + } + if (instanceInfo.getMimeTypeImage().size() == 0) { + holder.binding.buttonAttachImage.setEnabled(false); + } + if (instanceInfo.getMimeTypeVideo().size() == 0) { + holder.binding.buttonAttachVideo.setEnabled(false); + } + if (instanceInfo.getMimeTypeOther().size() == 0) { + holder.binding.buttonAttachManual.setEnabled(false); + } + } + holder.binding.attachmentChoicesPanel.setVisibility(View.VISIBLE); + }); + + //Disable buttons to attach media if max has been reached + if (statusDraft.media_attachments != null && statusDraft.media_attachments.size() >= instanceInfo.configuration.statusesConf.max_media_attachments) { + holder.binding.buttonAttachImage.setEnabled(false); + holder.binding.buttonAttachVideo.setEnabled(false); + holder.binding.buttonAttachAudio.setEnabled(false); + holder.binding.buttonAttachManual.setEnabled(false); + + } else { + holder.binding.buttonAttachImage.setEnabled(true); + holder.binding.buttonAttachVideo.setEnabled(true); + holder.binding.buttonAttachAudio.setEnabled(true); + holder.binding.buttonAttachManual.setEnabled(true); + } + holder.binding.buttonAttachAudio.setOnClickListener(v -> { + holder.binding.attachmentChoicesPanel.setVisibility(View.GONE); + pickupMedia(ComposeActivity.mediaType.AUDIO, position); + }); + holder.binding.buttonAttachImage.setOnClickListener(v -> { + holder.binding.attachmentChoicesPanel.setVisibility(View.GONE); + pickupMedia(ComposeActivity.mediaType.PHOTO, position); + }); + holder.binding.buttonAttachVideo.setOnClickListener(v -> { + holder.binding.attachmentChoicesPanel.setVisibility(View.GONE); + pickupMedia(ComposeActivity.mediaType.VIDEO, position); + }); + holder.binding.buttonAttachManual.setOnClickListener(v -> { + holder.binding.attachmentChoicesPanel.setVisibility(View.GONE); + pickupMedia(ComposeActivity.mediaType.ALL, position); + }); + if (statusDraft.visibility == null) { + statusDraft.visibility = BaseMainActivity.accountWeakReference.get().mastodon_account.source.privacy; + } + switch (statusDraft.visibility.toLowerCase()) { + case "public": + holder.binding.buttonVisibility.setImageResource(R.drawable.ic_compose_visibility_public); + statusDraft.visibility = MastodonHelper.visibility.PUBLIC.name(); + break; + case "unlisted": + holder.binding.buttonVisibility.setImageResource(R.drawable.ic_compose_visibility_unlisted); + statusDraft.visibility = MastodonHelper.visibility.UNLISTED.name(); + break; + case "private": + holder.binding.buttonVisibility.setImageResource(R.drawable.ic_compose_visibility_private); + statusDraft.visibility = MastodonHelper.visibility.PRIVATE.name(); + break; + case "direct": + holder.binding.buttonVisibility.setImageResource(R.drawable.ic_compose_visibility_direct); + statusDraft.visibility = MastodonHelper.visibility.DIRECT.name(); + break; + } + + holder.binding.buttonCloseAttachmentPanel.setOnClickListener(v -> holder.binding.attachmentChoicesPanel.setVisibility(View.GONE)); + holder.binding.buttonVisibility.setOnClickListener(v -> holder.binding.visibilityPanel.setVisibility(View.VISIBLE)); + holder.binding.buttonCloseVisibilityPanel.setOnClickListener(v -> holder.binding.visibilityPanel.setVisibility(View.GONE)); + holder.binding.buttonVisibilityDirect.setOnClickListener(v -> { + holder.binding.visibilityPanel.setVisibility(View.GONE); + holder.binding.buttonVisibility.setImageResource(R.drawable.ic_compose_visibility_direct); + statusDraft.visibility = MastodonHelper.visibility.DIRECT.name(); + }); + holder.binding.buttonVisibilityPrivate.setOnClickListener(v -> { + holder.binding.visibilityPanel.setVisibility(View.GONE); + holder.binding.buttonVisibility.setImageResource(R.drawable.ic_compose_visibility_private); + statusDraft.visibility = MastodonHelper.visibility.PRIVATE.name(); + }); + holder.binding.buttonVisibilityUnlisted.setOnClickListener(v -> { + holder.binding.visibilityPanel.setVisibility(View.GONE); + holder.binding.buttonVisibility.setImageResource(R.drawable.ic_compose_visibility_unlisted); + statusDraft.visibility = MastodonHelper.visibility.UNLISTED.name(); + }); + holder.binding.buttonVisibilityPublic.setOnClickListener(v -> { + holder.binding.visibilityPanel.setVisibility(View.GONE); + holder.binding.buttonVisibility.setImageResource(R.drawable.ic_compose_visibility_public); + statusDraft.visibility = MastodonHelper.visibility.PUBLIC.name(); + }); + holder.binding.buttonSensitive.setOnClickListener(v -> { + if (holder.binding.contentSpoiler.getVisibility() == View.VISIBLE) + holder.binding.contentSpoiler.setVisibility(View.GONE); + else + holder.binding.contentSpoiler.setVisibility(View.VISIBLE); + }); + //Last compose drawer + buttonVisibility(holder); + + holder.binding.buttonEmoji.setOnClickListener(v -> { + try { + displayEmojiPicker(holder); + } catch (DBException e) { + e.printStackTrace(); + } + }); + displayAttachments(holder, position, -1); + + manageMentions(context, statusDraft, holder); + //For some instances this value can be null, we have to transform the html content + if (statusDraft.text == null && statusDraft.content != null) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + statusDraft.text = new SpannableString(Html.fromHtml(statusDraft.content, Html.FROM_HTML_MODE_LEGACY)).toString(); + else + statusDraft.text = new SpannableString(Html.fromHtml(statusDraft.content)).toString(); + } + holder.binding.content.setText(statusDraft.text); + holder.binding.content.setSelection(statusDraft.cursorPosition); + if (statusDraft.setCursorToEnd) { + statusDraft.setCursorToEnd = false; + holder.binding.content.setSelection(holder.binding.content.getText().length()); + } + if (statusDraft.spoiler_text != null) { + holder.binding.contentSpoiler.setText(statusDraft.spoiler_text); + holder.binding.contentSpoiler.setSelection(holder.binding.contentSpoiler.getText().length()); + } else { + holder.binding.contentSpoiler.setText(""); + } + holder.binding.sensitiveMedia.setChecked(statusDraft.sensitive); + holder.binding.content.addTextChangedListener(initializeTextWatcher(holder)); + holder.binding.buttonPoll.setOnClickListener(v -> displayPollPopup(holder, statusDraft, position)); + holder.binding.buttonPoll.setOnClickListener(v -> displayPollPopup(holder, statusDraft, position)); + holder.binding.characterProgress.setMax(instanceInfo.configuration.statusesConf.max_characters); + holder.binding.contentSpoiler.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + } + + @Override + public void afterTextChanged(Editable s) { + statusList.get(holder.getAdapterPosition()).spoiler_text = s.toString(); + if (s.toString().trim().length() < 2) { + buttonVisibility(holder); + } + updateCharacterCount(holder); + } + }); + if (statusDraft.poll != null) { + ImageViewCompat.setImageTintList(holder.binding.buttonPoll, ColorStateList.valueOf(ContextCompat.getColor(context, R.color.cyanea_accent))); + } else { + ImageViewCompat.setImageTintList(holder.binding.buttonPoll, null); + } + + holder.binding.buttonPost.setOnClickListener(v -> { + manageDrafts.onSubmit(prepareDraft(statusList, this, account.instance, account.user_id)); + }); + } + + } + + + private void displayEmojiPicker(ComposeViewHolder holder) throws DBException { + if (emojis != null) { + emojis.clear(); + emojis = null; + } + + emojis = new EmojiInstance(context).getEmojiList(BaseMainActivity.currentInstance); + final AlertDialog.Builder builder = new AlertDialog.Builder(context, Helper.dialogStyle()); + int paddingPixel = 15; + float density = context.getResources().getDisplayMetrics().density; + int paddingDp = (int) (paddingPixel * density); + builder.setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss()); + builder.setTitle(R.string.insert_emoji); + if (emojis != null && emojis.size() > 0) { + GridView gridView = new GridView(context); + gridView.setAdapter(new EmojiAdapter(emojis)); + gridView.setNumColumns(5); + gridView.setOnItemClickListener((parent, view, position, id) -> { + holder.binding.content.getText().insert(holder.binding.content.getSelectionStart(), " :" + emojis.get(position).shortcode + ": "); + alertDialogEmoji.dismiss(); + }); + gridView.setPadding(paddingDp, paddingDp, paddingDp, paddingDp); + builder.setView(gridView); + } else { + TextView textView = new TextView(context); + textView.setText(context.getString(R.string.no_emoji)); + textView.setPadding(paddingDp, paddingDp, paddingDp, paddingDp); + builder.setView(textView); + } + alertDialogEmoji = builder.show(); + } + + private void displayPollPopup(ComposeAdapter.ComposeViewHolder holder, Status statusDraft, int position) { + AlertDialog.Builder alertPoll = new AlertDialog.Builder(context, Helper.dialogStyle()); + alertPoll.setTitle(R.string.create_poll); + ComposePollBinding composePollBinding = ComposePollBinding.inflate(LayoutInflater.from(context), new LinearLayout(context), false); + alertPoll.setView(composePollBinding.getRoot()); + int max_entry = 4; + int max_length = 25; + final int[] pollCountItem = {2}; + + if (instanceInfo != null && instanceInfo.configuration != null && instanceInfo.configuration.pollsConf != null) { + max_entry = instanceInfo.configuration.pollsConf.max_options; + max_length = instanceInfo.configuration.pollsConf.max_option_chars; + } + InputFilter[] fArray = new InputFilter[1]; + fArray[0] = new InputFilter.LengthFilter(max_length); + composePollBinding.option1.text.setFilters(fArray); + composePollBinding.option1.text.setHint(context.getString(R.string.poll_choice_s, 1)); + composePollBinding.option2.text.setFilters(fArray); + composePollBinding.option2.text.setHint(context.getString(R.string.poll_choice_s, 2)); + composePollBinding.option1.buttonRemove.setVisibility(View.GONE); + composePollBinding.option2.buttonRemove.setVisibility(View.GONE); + int finalMax_entry = max_entry; + composePollBinding.buttonAddOption.setOnClickListener(v -> { + if (pollCountItem[0] < finalMax_entry) { + ComposePollItemBinding composePollItemBinding = ComposePollItemBinding.inflate(LayoutInflater.from(context), new LinearLayout(context), false); + composePollItemBinding.text.setFilters(fArray); + composePollItemBinding.text.setHint(context.getString(R.string.poll_choice_s, (pollCountItem[0] + 1))); + LinearLayoutCompat viewItem = composePollItemBinding.getRoot(); + composePollBinding.optionsList.addView(composePollItemBinding.getRoot()); + composePollItemBinding.buttonRemove.setOnClickListener(view -> { + composePollBinding.optionsList.removeView(viewItem); + pollCountItem[0]--; + if (pollCountItem[0] >= finalMax_entry) { + composePollBinding.buttonAddOption.setVisibility(View.GONE); + } else { + composePollBinding.buttonAddOption.setVisibility(View.VISIBLE); + } + int childCount = composePollBinding.optionsListContainer.getChildCount(); + if (childCount > 2) { + for (int i = 2; i < childCount; i++) { + ((AppCompatEditText) composePollBinding.optionsListContainer.getChildAt(i)).setHint(context.getString(R.string.poll_choice_s, i + 1)); + } + } + + }); + } + pollCountItem[0]++; + if (pollCountItem[0] >= finalMax_entry) { + composePollBinding.buttonAddOption.setVisibility(View.GONE); + } else { + composePollBinding.buttonAddOption.setVisibility(View.VISIBLE); + } + + }); + + ArrayAdapter pollduration = ArrayAdapter.createFromResource(context, + R.array.poll_duration, android.R.layout.simple_spinner_dropdown_item); + + ArrayAdapter pollchoice = ArrayAdapter.createFromResource(context, + R.array.poll_choice_type, android.R.layout.simple_spinner_dropdown_item); + composePollBinding.pollType.setAdapter(pollchoice); + composePollBinding.pollDuration.setAdapter(pollduration); + composePollBinding.pollDuration.setSelection(4); + composePollBinding.pollType.setSelection(0); + if (statusDraft != null && statusDraft.poll != null && statusDraft.poll.options != null) { + int i = 1; + for (Poll.PollItem pollItem : statusDraft.poll.options) { + if (i == 1) { + if (pollItem.title != null) + composePollBinding.option1.text.setText(pollItem.title); + } else if (i == 2) { + if (pollItem.title != null) + composePollBinding.option2.text.setText(pollItem.title); + } else { + + ComposePollItemBinding composePollItemBinding = ComposePollItemBinding.inflate(LayoutInflater.from(context), new LinearLayout(context), false); + + composePollItemBinding.text.setFilters(fArray); + composePollItemBinding.text.setHint(context.getString(R.string.poll_choice_s, (pollCountItem[0] + 1))); + composePollBinding.optionsList.addView(composePollItemBinding.getRoot()); + composePollItemBinding.buttonRemove.setOnClickListener(view -> { + composePollBinding.optionsList.removeView(view); + pollCountItem[0]--; + }); + pollCountItem[0]++; + } + i++; + } + if (statusDraft.poll.options.size() >= max_entry) { + composePollBinding.buttonAddOption.setVisibility(View.GONE); + } + switch (statusDraft.poll.expire_in) { + case 300: + composePollBinding.pollDuration.setSelection(0); + break; + case 1800: + composePollBinding.pollDuration.setSelection(1); + break; + case 3600: + composePollBinding.pollDuration.setSelection(2); + break; + case 21600: + composePollBinding.pollDuration.setSelection(3); + break; + case 86400: + composePollBinding.pollDuration.setSelection(4); + break; + case 259200: + composePollBinding.pollDuration.setSelection(5); + break; + case 604800: + composePollBinding.pollDuration.setSelection(6); + break; + } + if (statusDraft.poll.multiple) + composePollBinding.pollType.setSelection(1); + else + composePollBinding.pollType.setSelection(0); + + + } + alertPoll.setNegativeButton(R.string.delete, (dialog, whichButton) -> { + if (statusDraft != null && statusDraft.poll != null) statusDraft.poll = null; + buttonState(holder); + dialog.dismiss(); + notifyItemChanged(position); + }); + alertPoll.setPositiveButton(R.string.validate, null); + final AlertDialog alertPollDiaslog = alertPoll.create(); + alertPollDiaslog.setOnShowListener(dialog -> { + + Button b = alertPollDiaslog.getButton(AlertDialog.BUTTON_POSITIVE); + b.setOnClickListener(view1 -> { + int poll_duration_pos = composePollBinding.pollDuration.getSelectedItemPosition(); + + int poll_choice_pos = composePollBinding.pollType.getSelectedItemPosition(); + String choice1 = composePollBinding.option1.text.getText().toString().trim(); + String choice2 = composePollBinding.option2.text.getText().toString().trim(); + + if (choice1.isEmpty() && choice2.isEmpty()) { + Toasty.error(context, context.getString(R.string.poll_invalid_choices), Toasty.LENGTH_SHORT).show(); + } else if (statusDraft != null) { + statusDraft.poll = new Poll(); + statusDraft.poll.multiple = (poll_choice_pos != 0); + int expire; + switch (poll_duration_pos) { + case 0: + expire = 300; + break; + case 1: + expire = 1800; + break; + case 2: + expire = 3600; + break; + case 3: + expire = 21600; + break; + case 4: + expire = 86400; + break; + case 5: + expire = 259200; + break; + case 6: + expire = 604800; + break; + default: + expire = 864000; + } + statusDraft.poll.expire_in = expire; + + List pollItems = new ArrayList<>(); + Poll.PollItem pollOption1 = new Poll.PollItem(); + pollOption1.title = choice1; + pollItems.add(pollOption1); + + Poll.PollItem pollOption2 = new Poll.PollItem(); + pollOption2.title = choice2; + pollItems.add(pollOption2); + + int childCount = composePollBinding.optionsListContainer.getChildCount(); + if (childCount > 2) { + for (int i = 2; i < childCount; i++) { + Poll.PollItem pollItem = new Poll.PollItem(); + pollItem.title = ((AppCompatEditText) composePollBinding.optionsListContainer.getChildAt(i)).getText().toString(); + pollItems.add(pollItem); + } + } + List options = new ArrayList<>(); + boolean doubleTitle = false; + for (Poll.PollItem po : pollItems) { + if (!options.contains(po.title.trim())) { + options.add(po.title.trim()); + } else { + doubleTitle = true; + } + } + if (!doubleTitle) { + statusDraft.poll.options = pollItems; + dialog.dismiss(); + } else { + Toasty.error(context, context.getString(R.string.poll_duplicated_entry), Toasty.LENGTH_SHORT).show(); + } + } + holder.binding.buttonPoll.setVisibility(View.VISIBLE); + buttonState(holder); + notifyItemChanged(position); + }); + }); + + alertPollDiaslog.show(); + } + + public interface ManageDrafts { + void onItemDraftAdded(int position); + + void onItemDraftDeleted(Status status, int position); + + void onSubmit(StatusDraft statusDraft); + } + + public static class StatusSimpleViewHolder extends RecyclerView.ViewHolder { + DrawerStatusSimpleBinding binding; + + StatusSimpleViewHolder(DrawerStatusSimpleBinding itemView) { + super(itemView.getRoot()); + binding = itemView; + } + } + + public static class ComposeViewHolder extends RecyclerView.ViewHolder { + public DrawerStatusComposeBinding binding; + + ComposeViewHolder(DrawerStatusComposeBinding itemView) { + super(itemView.getRoot()); + binding = itemView; + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/app/fedilab/android/ui/drawer/ContextAdapter.java b/app/src/main/java/app/fedilab/android/ui/drawer/ContextAdapter.java new file mode 100644 index 000000000..79fa33293 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/ui/drawer/ContextAdapter.java @@ -0,0 +1,89 @@ +package app.fedilab.android.ui.drawer; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + + +import static app.fedilab.android.ui.drawer.StatusAdapter.statusManagement; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.lifecycle.ViewModelProvider; +import androidx.lifecycle.ViewModelStoreOwner; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.List; + +import app.fedilab.android.client.mastodon.entities.Status; +import app.fedilab.android.databinding.DrawerStatusBinding; +import app.fedilab.android.viewmodel.mastodon.SearchVM; +import app.fedilab.android.viewmodel.mastodon.StatusesVM; + + +public class ContextAdapter extends RecyclerView.Adapter { + private final List statusList; + private final int TYPE_NORMAL = 0; + private final int TYPE_FOCUSED = 1; + private Context context; + + public ContextAdapter(List statusList) { + this.statusList = statusList; + } + + public int getCount() { + return statusList.size(); + } + + public Status getItem(int position) { + return statusList.get(position); + } + + @Override + public int getItemViewType(int position) { + return statusList.get(position).isFocused ? TYPE_FOCUSED : TYPE_NORMAL; + } + + @NonNull + @Override + public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + context = parent.getContext(); + DrawerStatusBinding itemBinding = DrawerStatusBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new StatusAdapter.StatusViewHolder(itemBinding); + } + + @Override + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) { + Status status = statusList.get(position); + StatusesVM statusesVM = new ViewModelProvider((ViewModelStoreOwner) context).get(StatusesVM.class); + SearchVM searchVM = new ViewModelProvider((ViewModelStoreOwner) context).get(SearchVM.class); + StatusAdapter.StatusViewHolder holder = (StatusAdapter.StatusViewHolder) viewHolder; + statusManagement(context, statusesVM, searchVM, holder, this, statusList, null, status, false, false); + //Hide/Show specific view + + } + + public long getItemId(int position) { + return position; + } + + @Override + public int getItemCount() { + return statusList.size(); + } + + +} \ No newline at end of file diff --git a/app/src/main/java/app/fedilab/android/ui/drawer/ConversationAdapter.java b/app/src/main/java/app/fedilab/android/ui/drawer/ConversationAdapter.java new file mode 100644 index 000000000..bb56fdd48 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/ui/drawer/ConversationAdapter.java @@ -0,0 +1,247 @@ +package app.fedilab.android.ui.drawer; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.appcompat.widget.LinearLayoutCompat; +import androidx.preference.PreferenceManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.request.RequestOptions; + +import java.util.ArrayList; +import java.util.List; +import java.util.Timer; +import java.util.TimerTask; + +import app.fedilab.android.R; +import app.fedilab.android.activities.ContextActivity; +import app.fedilab.android.client.mastodon.entities.Account; +import app.fedilab.android.client.mastodon.entities.Attachment; +import app.fedilab.android.client.mastodon.entities.Conversation; +import app.fedilab.android.client.mastodon.entities.Status; +import app.fedilab.android.databinding.DrawerConversationBinding; +import app.fedilab.android.databinding.ThumbnailBinding; +import app.fedilab.android.helper.Helper; +import app.fedilab.android.helper.MastodonHelper; + + +public class ConversationAdapter extends RecyclerView.Adapter { + private final List conversationList; + private Context context; + private boolean isExpended = false; + + public ConversationAdapter(List conversations) { + if (conversations == null) { + conversations = new ArrayList<>(); + } + this.conversationList = conversations; + } + + public int getCount() { + return conversationList.size(); + } + + public Conversation getItem(int position) { + return conversationList.get(position); + } + + @NonNull + @Override + public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + context = parent.getContext(); + DrawerConversationBinding itemBinding = DrawerConversationBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new ConversationHolder(itemBinding); + } + + @SuppressLint("ClickableViewAccessibility") + @Override + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) { + Conversation conversation = conversationList.get(position); + ConversationHolder holder = (ConversationHolder) viewHolder; + int theme_icons_color = -1; + int theme_statuses_color = -1; + int theme_text_color = -1; + final SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(context); + if (sharedpreferences.getBoolean("use_custom_theme", false)) { + //Getting custom colors + theme_icons_color = sharedpreferences.getInt("theme_icons_color", -1); + theme_statuses_color = sharedpreferences.getInt("theme_statuses_color", -1); + theme_text_color = sharedpreferences.getInt("theme_text_color", -1); + + } + if (theme_icons_color != -1) { + Helper.changeDrawableColor(context, R.drawable.ic_baseline_star_24, theme_icons_color); + Helper.changeDrawableColor(context, R.drawable.ic_repeat, theme_icons_color); + Helper.changeDrawableColor(context, R.drawable.ic_star_outline, theme_icons_color); + Helper.changeDrawableColor(context, R.drawable.ic_person, theme_icons_color); + } + if (theme_statuses_color != -1) { + holder.binding.container.setBackgroundColor(theme_statuses_color); + } + if (theme_text_color != -1) { + holder.binding.statusContent.setTextColor(theme_text_color); + holder.binding.spoiler.setTextColor(theme_text_color); + } + + //--- Profile Pictures for participants --- + holder.binding.participantsList.removeAllViews(); + for (Account account : conversation.accounts) { + ImageView imageView = new ImageView(context); + LinearLayoutCompat.LayoutParams lp = new LinearLayoutCompat.LayoutParams((int) Helper.convertDpToPixel(20, context), (int) Helper.convertDpToPixel(20, context)); + imageView.setScaleType(ImageView.ScaleType.CENTER_CROP); + lp.setMarginEnd((int) Helper.convertDpToPixel(6, context)); + imageView.setAdjustViewBounds(true); + imageView.setLayoutParams(lp); + MastodonHelper.loadPPMastodon(imageView, account); + holder.binding.participantsList.addView(imageView); + } + //---- SPOILER TEXT ----- + boolean expand_cw = sharedpreferences.getBoolean(context.getString(R.string.SET_EXPAND_CW), false); + if (conversation.last_status.spoiler_text != null && !conversation.last_status.spoiler_text.trim().isEmpty()) { + if (expand_cw || !conversation.last_status.sensitive) { + isExpended = true; + } + holder.binding.spoilerExpand.setOnClickListener(v -> { + isExpended = !isExpended; + notifyItemChanged(position); + }); + holder.binding.spoiler.setVisibility(View.VISIBLE); + holder.binding.spoiler.setText(conversation.last_status.span_spoiler_text, TextView.BufferType.SPANNABLE); + } else { + holder.binding.spoiler.setVisibility(View.GONE); + holder.binding.spoilerExpand.setVisibility(View.GONE); + holder.binding.spoiler.setText(null); + } + //--- MAIN CONTENT --- + holder.binding.statusContent.setText(conversation.last_status.span_content, TextView.BufferType.SPANNABLE); + //--- DATE --- + holder.binding.lastMessageDate.setText(Helper.dateDiff(context, conversation.last_status.created_at)); + + holder.binding.statusContent.setOnClickListener(v -> { + Intent intent = new Intent(context, ContextActivity.class); + intent.putExtra(Helper.ARG_STATUS, conversation.last_status); + context.startActivity(intent); + }); + + holder.binding.attachmentsListContainer.setOnTouchListener((v, event) -> { + if (event.getAction() == MotionEvent.ACTION_UP) { + Intent intent = new Intent(context, ContextActivity.class); + intent.putExtra(Helper.ARG_STATUS, conversation.last_status); + context.startActivity(intent); + } + return false; + }); + + displayAttachments(holder, position); + if (holder.timer != null) { + holder.timer.cancel(); + holder.timer = null; + } + + if (conversation.last_status.emojis != null && conversation.last_status.emojis.size() > 0) { + holder.timer = new Timer(); + holder.timer.scheduleAtFixedRate(new TimerTask() { + @Override + public void run() { + holder.binding.statusContent.invalidate(); + } + }, 100, 100); + } + } + + private void displayAttachments(ConversationAdapter.ConversationHolder holder, int position) { + if (conversationList.get(position).last_status != null) { + Status status = conversationList.get(position).last_status; + holder.binding.attachmentsList.removeAllViews(); + List attachmentList = status.media_attachments; + if (attachmentList != null && attachmentList.size() > 0) { + for (Attachment attachment : attachmentList) { + ThumbnailBinding thumbnailBinding = ThumbnailBinding.inflate(LayoutInflater.from(context), holder.binding.attachmentsList, false); + thumbnailBinding.buttonPlay.setVisibility(View.GONE); + if (attachment.type.compareToIgnoreCase("image") == 0) { + Glide.with(thumbnailBinding.preview.getContext()) + .load(attachment.preview_url) + .into(thumbnailBinding.preview); + } else if (attachment.type.compareToIgnoreCase("video") == 0 || attachment.type.compareToIgnoreCase("gifv") == 0) { + thumbnailBinding.buttonPlay.setVisibility(View.VISIBLE); + long interval = 2000; + RequestOptions options = new RequestOptions().frame(interval); + Glide.with(thumbnailBinding.preview.getContext()).asBitmap() + .load(attachment.preview_url) + .apply(options) + .into(thumbnailBinding.preview); + } else if (attachment.type.compareToIgnoreCase("audio") == 0) { + Glide.with(thumbnailBinding.preview.getContext()) + .load(R.drawable.ic_baseline_audio_file_24) + .into(thumbnailBinding.preview); + } else { + Glide.with(thumbnailBinding.preview.getContext()) + .load(R.drawable.ic_baseline_insert_drive_file_24) + .into(thumbnailBinding.preview); + } + holder.binding.attachmentsList.addView(thumbnailBinding.getRoot()); + } + holder.binding.attachmentsList.setVisibility(View.VISIBLE); + } else { + holder.binding.attachmentsList.setVisibility(View.GONE); + } + } else { + holder.binding.attachmentsList.setVisibility(View.GONE); + } + } + + public long getItemId(int position) { + return position; + } + + @Override + public int getItemCount() { + return conversationList.size(); + } + + @Override + public void onViewRecycled(@NonNull RecyclerView.ViewHolder holder) { + super.onViewRecycled(holder); + if (holder instanceof ConversationHolder && ((ConversationHolder) holder).timer != null) { + ((ConversationHolder) holder).timer.cancel(); + } + } + + static class ConversationHolder extends RecyclerView.ViewHolder { + DrawerConversationBinding binding; + Timer timer; + + ConversationHolder(DrawerConversationBinding itemView) { + super(itemView.getRoot()); + binding = itemView; + } + } + + +} \ No newline at end of file diff --git a/app/src/main/java/app/fedilab/android/ui/drawer/EmojiAdapter.java b/app/src/main/java/app/fedilab/android/ui/drawer/EmojiAdapter.java new file mode 100644 index 000000000..05f110ac6 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/ui/drawer/EmojiAdapter.java @@ -0,0 +1,125 @@ +package app.fedilab.android.ui.drawer; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + + +import android.content.SharedPreferences; +import android.graphics.drawable.Drawable; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.preference.PreferenceManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.request.target.CustomTarget; +import com.bumptech.glide.request.transition.Transition; +import com.github.penfeizhou.animation.apng.APNGDrawable; +import com.github.penfeizhou.animation.apng.decode.APNGParser; +import com.github.penfeizhou.animation.gif.GifDrawable; +import com.github.penfeizhou.animation.gif.decode.GifParser; + +import java.io.File; +import java.util.List; + +import app.fedilab.android.R; +import app.fedilab.android.client.mastodon.entities.Emoji; +import app.fedilab.android.databinding.DrawerEmojiPickerBinding; + + +public class EmojiAdapter extends BaseAdapter { + private final List emojiList; + + + public EmojiAdapter(List emojiList) { + this.emojiList = emojiList; + } + + public int getCount() { + return emojiList.size(); + } + + public Emoji getItem(int position) { + return emojiList.get(position); + } + + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + final Emoji emoji = emojiList.get(position); + EmojiViewHolder holder; + if (convertView == null) { + DrawerEmojiPickerBinding drawerEmojiPickerBinding = DrawerEmojiPickerBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + holder = new EmojiViewHolder(drawerEmojiPickerBinding); + holder.view = drawerEmojiPickerBinding.getRoot(); + holder.view.setTag(holder); + } else { + holder = (EmojiViewHolder) convertView.getTag(); + } + + SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(holder.view.getContext()); + boolean disableAnimatedEmoji = sharedpreferences.getBoolean(parent.getContext().getString(R.string.SET_DISABLE_ANIMATED_EMOJI), false); + Glide.with(holder.binding.imgCustomEmoji.getContext()) + .asFile() + .load(!disableAnimatedEmoji ? emoji.url : emoji.static_url) + .into(new CustomTarget() { + @Override + public void onResourceReady(@NonNull File resource, @Nullable Transition transition) { + if (APNGParser.isAPNG(resource.getAbsolutePath())) { + APNGDrawable apngDrawable = APNGDrawable.fromFile(resource.getAbsolutePath()); + holder.binding.imgCustomEmoji.setImageDrawable(apngDrawable); + } else if (GifParser.isGif(resource.getAbsolutePath())) { + GifDrawable gifDrawable = GifDrawable.fromFile(resource.getAbsolutePath()); + holder.binding.imgCustomEmoji.setImageDrawable(gifDrawable); + } else { + Drawable drawable = Drawable.createFromPath(resource.getAbsolutePath()); + holder.binding.imgCustomEmoji.setImageDrawable(drawable); + } + } + + @Override + public void onLoadCleared(@Nullable Drawable placeholder) { + + } + }); + + return holder.view; + } + + + public long getItemId(int position) { + return position; + } + + + public int getItemCount() { + return emojiList.size(); + } + + public static class EmojiViewHolder extends RecyclerView.ViewHolder { + DrawerEmojiPickerBinding binding; + private View view; + + EmojiViewHolder(DrawerEmojiPickerBinding itemView) { + super(itemView.getRoot()); + this.view = itemView.getRoot(); + binding = itemView; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/fedilab/android/ui/drawer/EmojiSearchAdapter.java b/app/src/main/java/app/fedilab/android/ui/drawer/EmojiSearchAdapter.java new file mode 100644 index 000000000..5f6dd03ff --- /dev/null +++ b/app/src/main/java/app/fedilab/android/ui/drawer/EmojiSearchAdapter.java @@ -0,0 +1,155 @@ +package app.fedilab.android.ui.drawer; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import android.content.Context; +import android.content.SharedPreferences; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.Filter; +import android.widget.Filterable; + +import androidx.annotation.NonNull; +import androidx.preference.PreferenceManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.bumptech.glide.Glide; + +import java.util.ArrayList; +import java.util.List; + +import app.fedilab.android.R; +import app.fedilab.android.client.mastodon.entities.Emoji; +import app.fedilab.android.databinding.DrawerEmojiSearchBinding; + + +public class EmojiSearchAdapter extends ArrayAdapter implements Filterable { + + private final List emojis; + private final List tempEmojis; + private final List suggestions; + private final Filter searchFilter = new Filter() { + @Override + public CharSequence convertResultToString(Object resultValue) { + Emoji emoji = (Emoji) resultValue; + return emoji.shortcode; + } + + @Override + protected FilterResults performFiltering(CharSequence constraint) { + if (constraint != null) { + suggestions.clear(); + suggestions.addAll(tempEmojis); + FilterResults filterResults = new FilterResults(); + filterResults.values = suggestions; + filterResults.count = suggestions.size(); + return filterResults; + } else { + return new FilterResults(); + } + + } + + @Override + protected void publishResults(CharSequence constraint, FilterResults results) { + try { + ArrayList c = (ArrayList) results.values; + if (results.count > 0) { + clear(); + addAll(c); + notifyDataSetChanged(); + } else { + clear(); + notifyDataSetChanged(); + } + } catch (Exception ignored) { + } + + + } + }; + private final Context context; + + public EmojiSearchAdapter(Context context, List emojis) { + super(context, android.R.layout.simple_list_item_1, emojis); + this.emojis = emojis; + this.tempEmojis = new ArrayList<>(emojis); + this.suggestions = new ArrayList<>(emojis); + this.context = context; + } + + + @Override + public int getCount() { + return emojis.size(); + } + + @Override + public Emoji getItem(int position) { + return emojis.get(position); + } + + @Override + public long getItemId(int position) { + return position; + } + + @NonNull + @Override + public View getView(final int position, View convertView, @NonNull ViewGroup parent) { + + final Emoji emoji = emojis.get(position); + EmojiSearchViewHolder holder; + if (convertView == null) { + DrawerEmojiSearchBinding drawerEmojiSearchBinding = DrawerEmojiSearchBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + holder = new EmojiSearchViewHolder(drawerEmojiSearchBinding); + holder.view = drawerEmojiSearchBinding.getRoot(); + holder.view.setTag(holder); + } else { + holder = (EmojiSearchViewHolder) convertView.getTag(); + } + if (emoji != null) { + holder.binding.emojiShortcode.setText(String.format("%s", emoji.shortcode)); + SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(context); + boolean disableGif = sharedpreferences.getBoolean(context.getString(R.string.SET_DISABLE_GIF), false); + String targetedUrl = disableGif ? emoji.static_url : emoji.url; + Glide.with(holder.view.getContext()) + .asDrawable() + .load(targetedUrl) + .into(holder.binding.emojiIcon); + } + return holder.view; + } + + @NonNull + @Override + public Filter getFilter() { + return searchFilter; + } + + public static class EmojiSearchViewHolder extends RecyclerView.ViewHolder { + DrawerEmojiSearchBinding binding; + private View view; + + EmojiSearchViewHolder(DrawerEmojiSearchBinding itemView) { + super(itemView.getRoot()); + this.view = itemView.getRoot(); + binding = itemView; + } + } + +} diff --git a/app/src/main/java/app/fedilab/android/ui/drawer/FilterAdapter.java b/app/src/main/java/app/fedilab/android/ui/drawer/FilterAdapter.java new file mode 100644 index 000000000..84a77ebec --- /dev/null +++ b/app/src/main/java/app/fedilab/android/ui/drawer/FilterAdapter.java @@ -0,0 +1,127 @@ +package app.fedilab.android.ui.drawer; +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import androidx.lifecycle.ViewModelProvider; +import androidx.lifecycle.ViewModelStoreOwner; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.List; + +import app.fedilab.android.BaseMainActivity; +import app.fedilab.android.R; +import app.fedilab.android.activities.FilterActivity; +import app.fedilab.android.client.mastodon.entities.Filter; +import app.fedilab.android.databinding.DrawerFilterBinding; +import app.fedilab.android.helper.Helper; +import app.fedilab.android.viewmodel.mastodon.AccountsVM; + + +public class FilterAdapter extends RecyclerView.Adapter { + + private final List filters; + private final FilterAdapter filterAdapter; + public Delete delete; + private Context context; + + public FilterAdapter(List filters) { + this.filters = filters; + this.filterAdapter = this; + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public int getItemCount() { + return filters.size(); + } + + @NonNull + @Override + public FilterAdapter.FilterViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + context = parent.getContext(); + DrawerFilterBinding itemBinding = DrawerFilterBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new FilterAdapter.FilterViewHolder(itemBinding); + } + + @Override + public void onBindViewHolder(@NonNull FilterViewHolder holder, int position) { + Filter filter = filters.get(position); + if (filter.phrase != null) { + holder.binding.filterWord.setText(filter.phrase); + } + StringBuilder contextString = new StringBuilder(); + if (filter.context != null) + for (String ct : filter.context) + contextString.append(ct).append(" "); + holder.binding.filterContext.setText(contextString.toString()); + holder.binding.editFilter.setOnClickListener(v -> FilterActivity.addEditFilter(context, filter, filter1 -> { + if (filter1 != null) { + BaseMainActivity.mainFilters.get(position).phrase = filter1.phrase; + BaseMainActivity.mainFilters.get(position).context = filter1.context; + BaseMainActivity.mainFilters.get(position).whole_word = filter1.whole_word; + BaseMainActivity.mainFilters.get(position).irreversible = filter1.irreversible; + BaseMainActivity.mainFilters.get(position).expires_at = filter1.expires_at; + } + filterAdapter.notifyItemChanged(position); + })); + holder.binding.deleteFilter.setOnClickListener(v -> { + AlertDialog.Builder builder = new AlertDialog.Builder(context, Helper.dialogStyle()); + builder.setTitle(R.string.action_filter_delete); + builder.setMessage(R.string.action_lists_confirm_delete); + builder.setIcon(android.R.drawable.ic_dialog_alert) + .setPositiveButton(R.string.yes, (dialog, which) -> { + AccountsVM accountsVM = new ViewModelProvider((ViewModelStoreOwner) context).get(AccountsVM.class); + accountsVM.removeFilter(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, filter.id); + filters.remove(filter); + if (filters.size() == 0) { + delete.allFiltersDeleted(); + } + filterAdapter.notifyItemRemoved(holder.getAbsoluteAdapterPosition()); + dialog.dismiss(); + }) + .setNegativeButton(R.string.no, (dialog, which) -> dialog.dismiss()) + .show(); + }); + } + + + public interface FilterAction { + void callback(Filter filter); + } + + public interface Delete { + void allFiltersDeleted(); + } + + public static class FilterViewHolder extends RecyclerView.ViewHolder { + DrawerFilterBinding binding; + + FilterViewHolder(DrawerFilterBinding itemView) { + super(itemView.getRoot()); + binding = itemView; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/fedilab/android/ui/drawer/IdentityProofsAdapter.java b/app/src/main/java/app/fedilab/android/ui/drawer/IdentityProofsAdapter.java new file mode 100644 index 000000000..517e74a41 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/ui/drawer/IdentityProofsAdapter.java @@ -0,0 +1,83 @@ +package app.fedilab.android.ui.drawer; +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.List; + +import app.fedilab.android.R; +import app.fedilab.android.client.mastodon.entities.IdentityProof; +import app.fedilab.android.databinding.DrawerIdentityProofsBinding; +import app.fedilab.android.helper.Helper; + + +public class IdentityProofsAdapter extends RecyclerView.Adapter { + + private final List identityProofList; + private Context context; + + public IdentityProofsAdapter(List identityProofs) { + this.identityProofList = identityProofs; + } + + public IdentityProof getItem(int position) { + return identityProofList.get(position); + } + + + @NonNull + @Override + public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + DrawerIdentityProofsBinding itemBinding = DrawerIdentityProofsBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new IdentityProofViewHolder(itemBinding); + } + + @Override + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int i) { + final IdentityProofViewHolder holder = (IdentityProofViewHolder) viewHolder; + final IdentityProof identityProof = getItem(i); + holder.binding.proofName.setText(String.format("@%s", identityProof.provider_username)); + holder.binding.proofName.setOnClickListener(v -> Helper.openBrowser(context, identityProof.profile_url)); + holder.binding.proofNameNetwork.setText(context.getString(R.string.verified_by, identityProof.provider, Helper.shortDateToString(identityProof.updated_at))); + holder.binding.proofContainer.setOnClickListener(v -> Helper.openBrowser(context, identityProof.profile_url)); + holder.binding.proofNameNetwork.setOnClickListener(v -> Helper.openBrowser(context, identityProof.proof_url)); + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public int getItemCount() { + return identityProofList.size(); + } + + public static class IdentityProofViewHolder extends RecyclerView.ViewHolder { + DrawerIdentityProofsBinding binding; + + IdentityProofViewHolder(DrawerIdentityProofsBinding itemView) { + super(itemView.getRoot()); + binding = itemView; + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/app/fedilab/android/ui/drawer/InstanceRegAdapter.java b/app/src/main/java/app/fedilab/android/ui/drawer/InstanceRegAdapter.java new file mode 100644 index 000000000..1604d2410 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/ui/drawer/InstanceRegAdapter.java @@ -0,0 +1,103 @@ +package app.fedilab.android.ui.drawer; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.load.resource.bitmap.FitCenter; +import com.bumptech.glide.load.resource.bitmap.RoundedCorners; +import com.bumptech.glide.request.RequestOptions; + +import java.util.List; + +import app.fedilab.android.R; +import app.fedilab.android.client.mastodon.entities.JoinMastodonInstance; +import app.fedilab.android.databinding.DrawerInstanceRegBinding; +import app.fedilab.android.helper.Helper; + + +public class InstanceRegAdapter extends RecyclerView.Adapter { + private final List joinMastodonInstanceList; + public RecyclerViewClickListener itemListener; + private Context context; + private ViewHolder holder; + + public InstanceRegAdapter(List joinMastodonInstanceList) { + this.joinMastodonInstanceList = joinMastodonInstanceList; + } + + public int getCount() { + return joinMastodonInstanceList.size(); + } + + public JoinMastodonInstance getItem(int position) { + return joinMastodonInstanceList.get(position); + } + + @NonNull + @Override + public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + context = parent.getContext(); + DrawerInstanceRegBinding itemBinding = DrawerInstanceRegBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new ViewHolder(itemBinding); + } + + @Override + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) { + JoinMastodonInstance joinMastodonInstance = joinMastodonInstanceList.get(position); + + holder = (ViewHolder) viewHolder; + holder.binding.instanceCountUser.setText(context.getString(R.string.users, Helper.withSuffix(joinMastodonInstance.total_users))); + holder.binding.instanceDescription.setText(joinMastodonInstance.description); + holder.binding.instanceHost.setText(joinMastodonInstance.domain); + holder.binding.instanceVersion.setText(String.format("%s - %s", joinMastodonInstance.categories, joinMastodonInstance.version)); + Glide.with(context) + .load(joinMastodonInstance.proxied_thumbnail) + .apply(new RequestOptions().transform(new FitCenter(), new RoundedCorners(10))) + .into(holder.binding.instancePp); + + holder.binding.getRoot().setOnClickListener(v -> itemListener.recyclerViewListClicked(v, position)); + } + + public long getItemId(int position) { + return position; + } + + @Override + public int getItemCount() { + return joinMastodonInstanceList.size(); + } + + public interface RecyclerViewClickListener { + void recyclerViewListClicked(View v, int position); + } + + static class ViewHolder extends RecyclerView.ViewHolder { + DrawerInstanceRegBinding binding; + + ViewHolder(DrawerInstanceRegBinding itemView) { + super(itemView.getRoot()); + binding = itemView; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/fedilab/android/ui/drawer/MastodonListAdapter.java b/app/src/main/java/app/fedilab/android/ui/drawer/MastodonListAdapter.java new file mode 100644 index 000000000..ccb7b3f7e --- /dev/null +++ b/app/src/main/java/app/fedilab/android/ui/drawer/MastodonListAdapter.java @@ -0,0 +1,78 @@ +package app.fedilab.android.ui.drawer; +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.List; + +import app.fedilab.android.client.mastodon.entities.MastodonList; +import app.fedilab.android.databinding.DrawerListBinding; + +public class MastodonListAdapter extends RecyclerView.Adapter { + private final List mastodonListList; + public ActionOnList actionOnList; + + public MastodonListAdapter(List mastodonList) { + this.mastodonListList = mastodonList; + } + + + public int getCount() { + return mastodonListList.size(); + } + + public MastodonList getItem(int position) { + return mastodonListList.get(position); + } + + @NonNull + @Override + public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + DrawerListBinding itemBinding = DrawerListBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new ListViewHolder(itemBinding); + } + + @Override + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) { + MastodonList mastodonList = mastodonListList.get(position); + ListViewHolder holder = (ListViewHolder) viewHolder; + holder.binding.title.setText(mastodonList.title); + holder.binding.title.setOnClickListener(v -> actionOnList.click(mastodonList)); + } + + @Override + public int getItemCount() { + return mastodonListList.size(); + } + + + public interface ActionOnList { + void click(MastodonList mastodonList); + } + + public static class ListViewHolder extends RecyclerView.ViewHolder { + DrawerListBinding binding; + + ListViewHolder(DrawerListBinding itemView) { + super(itemView.getRoot()); + binding = itemView; + } + } +} diff --git a/app/src/main/java/app/fedilab/android/ui/drawer/NotificationAdapter.java b/app/src/main/java/app/fedilab/android/ui/drawer/NotificationAdapter.java new file mode 100644 index 000000000..4663a19c3 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/ui/drawer/NotificationAdapter.java @@ -0,0 +1,203 @@ +package app.fedilab.android.ui.drawer; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + + +import static app.fedilab.android.ui.drawer.StatusAdapter.statusManagement; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.core.app.ActivityOptionsCompat; +import androidx.lifecycle.ViewModelProvider; +import androidx.lifecycle.ViewModelStoreOwner; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.List; +import java.util.Locale; + +import app.fedilab.android.R; +import app.fedilab.android.activities.ProfileActivity; +import app.fedilab.android.client.mastodon.entities.Notification; +import app.fedilab.android.databinding.DrawerFollowBinding; +import app.fedilab.android.databinding.DrawerStatusNotificationBinding; +import app.fedilab.android.helper.Helper; +import app.fedilab.android.helper.MastodonHelper; +import app.fedilab.android.viewmodel.mastodon.SearchVM; +import app.fedilab.android.viewmodel.mastodon.StatusesVM; + + +public class NotificationAdapter extends RecyclerView.Adapter { + private final List notificationList; + private final int TYPE_FOLLOW = 0; + private final int TYPE_FOLLOW_REQUEST = 1; + private final int TYPE_MENTION = 2; + private final int TYPE_REBLOG = 3; + private final int TYPE_FAVOURITE = 4; + private final int TYPE_POLL = 5; + private final int TYPE_STATUS = 6; + private Context context; + + public NotificationAdapter(List notificationList) { + this.notificationList = notificationList; + } + + public int getCount() { + return notificationList.size(); + } + + public Notification getItem(int position) { + return notificationList.get(position); + } + + @Override + public int getItemViewType(int position) { + String type = notificationList.get(position).type; + switch (type) { + case "follow": + return TYPE_FOLLOW; + case "follow_request": + return TYPE_FOLLOW_REQUEST; + case "mention": + return TYPE_MENTION; + case "reblog": + return TYPE_REBLOG; + case "favourite": + return TYPE_FAVOURITE; + case "poll": + return TYPE_POLL; + case "status": + return TYPE_STATUS; + } + return super.getItemViewType(position); + } + + @NonNull + @Override + public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + context = parent.getContext(); + if (viewType == TYPE_FOLLOW || viewType == TYPE_FOLLOW_REQUEST) { + DrawerFollowBinding itemBinding = DrawerFollowBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new ViewHolderFollow(itemBinding); + } else { + DrawerStatusNotificationBinding itemBinding = DrawerStatusNotificationBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new StatusAdapter.StatusViewHolder(itemBinding); + } + } + + @Override + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) { + Notification notification = notificationList.get(position); + if (getItemViewType(position) == TYPE_FOLLOW || getItemViewType(position) == TYPE_FOLLOW_REQUEST) { + ViewHolderFollow holderFollow = (ViewHolderFollow) viewHolder; + MastodonHelper.loadPPMastodon(holderFollow.binding.avatar, notification.account); + holderFollow.binding.displayName.setText(notification.account.display_name); + holderFollow.binding.username.setText(String.format("@%s", notification.account.acct)); + if (getItemViewType(position) == TYPE_FOLLOW_REQUEST) { + holderFollow.binding.rejectButton.setVisibility(View.VISIBLE); + holderFollow.binding.acceptButton.setVisibility(View.VISIBLE); + holderFollow.binding.title.setText(R.string.follow_request); + } else { + holderFollow.binding.rejectButton.setVisibility(View.GONE); + holderFollow.binding.acceptButton.setVisibility(View.GONE); + holderFollow.binding.title.setText(R.string.follow); + } + holderFollow.binding.avatar.setOnClickListener(v -> { + Intent intent = new Intent(context, ProfileActivity.class); + Bundle b = new Bundle(); + b.putSerializable(Helper.ARG_ACCOUNT, notification.account); + intent.putExtras(b); + ActivityOptionsCompat options = ActivityOptionsCompat + .makeSceneTransitionAnimation((Activity) context, holderFollow.binding.avatar, context.getString(R.string.activity_porfile_pp)); + // start the new activity + context.startActivity(intent, options.toBundle()); + }); + } else { + StatusAdapter.StatusViewHolder holderStatus = (StatusAdapter.StatusViewHolder) viewHolder; + StatusesVM statusesVM = new ViewModelProvider((ViewModelStoreOwner) context).get(StatusesVM.class); + SearchVM searchVM = new ViewModelProvider((ViewModelStoreOwner) context).get(SearchVM.class); + statusManagement(context, statusesVM, searchVM, holderStatus, this, null, notificationList, notification.status, false, false); + holderStatus.bindingNotification.containerTransparent.setAlpha(.3f); + if (getItemViewType(position) == TYPE_MENTION || getItemViewType(position) == TYPE_STATUS) { + holderStatus.bindingNotification.status.actionButtons.setVisibility(View.VISIBLE); + String title = ""; + if (getItemViewType(position) == TYPE_MENTION) { + title = String.format(Locale.getDefault(), "%s %s", notification.account.display_name, context.getString(R.string.notif_mention)); + } else if (getItemViewType(position) == TYPE_STATUS) { + title = String.format(Locale.getDefault(), "%s %s", notification.account.display_name, context.getString(R.string.notif_status)); + } + holderStatus.bindingNotification.status.displayName.setText(title); + holderStatus.bindingNotification.status.username.setText(String.format("@%s", notification.account.acct)); + holderStatus.bindingNotification.containerTransparent.setAlpha(.1f); + if (notification.status != null && notification.status.visibility.equalsIgnoreCase("direct")) { + holderStatus.bindingNotification.containerTransparent.setVisibility(View.GONE); + } else { + holderStatus.bindingNotification.containerTransparent.setVisibility(View.VISIBLE); + holderStatus.bindingNotification.containerTransparent.setAlpha(.1f); + } + } else { + holderStatus.bindingNotification.containerTransparent.setVisibility(View.VISIBLE); + String title = ""; + MastodonHelper.loadPPMastodon(holderStatus.binding.avatar, notification.account); + if (getItemViewType(position) == TYPE_FAVOURITE) { + title = String.format(Locale.getDefault(), "%s %s", notification.account.display_name, context.getString(R.string.notif_favourite)); + } else if (getItemViewType(position) == TYPE_REBLOG) { + title = String.format(Locale.getDefault(), "%s %s", notification.account.display_name, context.getString(R.string.notif_reblog)); + } else if (getItemViewType(position) == TYPE_POLL) { + title = context.getString(R.string.notif_poll); + } + holderStatus.bindingNotification.status.avatar.setOnClickListener(v -> { + Intent intent = new Intent(context, ProfileActivity.class); + Bundle b = new Bundle(); + b.putSerializable(Helper.ARG_ACCOUNT, notification.account); + intent.putExtras(b); + ActivityOptionsCompat options = ActivityOptionsCompat + .makeSceneTransitionAnimation((Activity) context, holderStatus.bindingNotification.status.avatar, context.getString(R.string.activity_porfile_pp)); + // start the new activity + context.startActivity(intent, options.toBundle()); + }); + holderStatus.bindingNotification.status.displayName.setText(title); + holderStatus.bindingNotification.status.username.setText(String.format("@%s", notification.account.acct)); + holderStatus.bindingNotification.status.actionButtons.setVisibility(View.GONE); + } + } + } + + + public long getItemId(int position) { + return position; + } + + @Override + public int getItemCount() { + return notificationList.size(); + } + + + static class ViewHolderFollow extends RecyclerView.ViewHolder { + DrawerFollowBinding binding; + + ViewHolderFollow(DrawerFollowBinding itemView) { + super(itemView.getRoot()); + binding = itemView; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/fedilab/android/ui/drawer/ReorderTabAdapter.java b/app/src/main/java/app/fedilab/android/ui/drawer/ReorderTabAdapter.java new file mode 100644 index 000000000..83effc891 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/ui/drawer/ReorderTabAdapter.java @@ -0,0 +1,206 @@ +package app.fedilab.android.ui.drawer; +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + + +import android.annotation.SuppressLint; +import android.content.Context; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.ViewGroup; +import android.widget.Toast; + +import androidx.core.content.ContextCompat; +import androidx.recyclerview.widget.RecyclerView; + +import org.jetbrains.annotations.NotNull; + +import java.util.Collections; + +import app.fedilab.android.R; +import app.fedilab.android.client.entities.Pinned; +import app.fedilab.android.client.entities.Timeline; +import app.fedilab.android.client.entities.app.PinnedTimeline; +import app.fedilab.android.databinding.DrawerReorderBinding; +import app.fedilab.android.exception.DBException; +import app.fedilab.android.helper.itemtouchhelper.ItemTouchHelperAdapter; +import app.fedilab.android.helper.itemtouchhelper.ItemTouchHelperViewHolder; +import app.fedilab.android.helper.itemtouchhelper.OnStartDragListener; +import app.fedilab.android.helper.itemtouchhelper.OnUndoListener; +import es.dmoral.toasty.Toasty; + + +/** + * Simple RecyclerView.Adapter that implements {@link ItemTouchHelperAdapter} to respond to move and + * dismiss events from a {@link androidx.recyclerview.widget.ItemTouchHelper}. + * + * @author Paul Burke (ipaulpro) + */ +public class ReorderTabAdapter extends RecyclerView.Adapter implements ItemTouchHelperAdapter { + + private final OnStartDragListener mDragStartListener; + private final OnUndoListener mUndoListener; + private final Pinned pinned; + private Context context; + + public ReorderTabAdapter(Pinned pinned, OnStartDragListener dragStartListener, OnUndoListener undoListener) { + this.mDragStartListener = dragStartListener; + this.mUndoListener = undoListener; + this.pinned = pinned; + } + + @NotNull + @Override + public ReorderViewHolder onCreateViewHolder(@NotNull ViewGroup parent, int viewType) { + context = parent.getContext(); + DrawerReorderBinding itemBinding = DrawerReorderBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new ReorderViewHolder(itemBinding); + } + + @SuppressLint("ClickableViewAccessibility") + @Override + public void onBindViewHolder(@NotNull final RecyclerView.ViewHolder viewHolder, int position) { + + ReorderViewHolder holder = (ReorderViewHolder) viewHolder; + + switch (pinned.pinnedTimelines.get(position).type) { + case REMOTE: + switch (pinned.pinnedTimelines.get(position).remoteInstance.type) { + case PEERTUBE: + holder.binding.icon.setImageResource(R.drawable.peertube_icon); + break; + case MASTODON: + holder.binding.icon.setImageResource(R.drawable.mastodon_icon_item); + break; + case PIXELFED: + holder.binding.icon.setImageResource(R.drawable.pixelfed); + break; + case MISSKEY: + holder.binding.icon.setImageResource(R.drawable.misskey); + break; + case GNU: + holder.binding.icon.setImageResource(R.drawable.ic_gnu_social); + break; + case NITTER: + holder.binding.icon.setImageResource(R.drawable.nitter); + break; + } + holder.binding.text.setText(pinned.pinnedTimelines.get(position).remoteInstance.host); + break; + case TAG: + holder.binding.icon.setImageResource(R.drawable.ic_baseline_label_24); + if (pinned.pinnedTimelines.get(position).tagTimeline.displayName != null) + holder.binding.text.setText(pinned.pinnedTimelines.get(position).tagTimeline.displayName); + else + holder.binding.text.setText(pinned.pinnedTimelines.get(position).tagTimeline.name); + break; + case LIST: + holder.binding.icon.setImageResource(R.drawable.ic_baseline_view_list_24); + holder.binding.text.setText(pinned.pinnedTimelines.get(position).mastodonList.title); + break; + } + + + if (pinned.pinnedTimelines.get(position).displayed) { + holder.binding.hide.setImageResource(R.drawable.ic_baseline_visibility_24); + } else { + holder.binding.hide.setImageResource(R.drawable.ic_baseline_visibility_off_24); + } + + holder.binding.hide.setOnClickListener(v -> { + pinned.pinnedTimelines.get(position).displayed = !pinned.pinnedTimelines.get(position).displayed; + if (pinned.pinnedTimelines.get(position).displayed) { + holder.binding.hide.setImageResource(R.drawable.ic_baseline_visibility_24); + } else { + holder.binding.hide.setImageResource(R.drawable.ic_baseline_visibility_off_24); + } + new Thread(() -> { + try { + new Pinned(context).updatePinned(pinned); + } catch (DBException e) { + e.printStackTrace(); + } + }).start(); + }); + + // Start a drag whenever the handle view it touched + holder.binding.handle.setOnTouchListener((v, event) -> { + if (event.getAction() == MotionEvent.ACTION_DOWN) { + mDragStartListener.onStartDrag(holder); + return true; + } + return false; + }); + + } + + @Override + public void onItemDismiss(int position) { + PinnedTimeline item = pinned.pinnedTimelines.get(position); + if (item.type == Timeline.TimeLineEnum.TAG || item.type == Timeline.TimeLineEnum.REMOTE || item.type == Timeline.TimeLineEnum.LIST) { + mUndoListener.onUndo(item, position); + pinned.pinnedTimelines.remove(position); + notifyItemRemoved(position); + } else { + notifyItemChanged(position); + Toasty.info(context, context.getString(R.string.warning_main_timeline), Toast.LENGTH_SHORT).show(); + } + } + + @Override + public boolean onItemMove(int fromPosition, int toPosition) { + Collections.swap(pinned.pinnedTimelines, fromPosition, toPosition); + //update position value + for (int j = 0; j < pinned.pinnedTimelines.size(); j++) { + pinned.pinnedTimelines.get(j).position = j; + } + notifyItemMoved(fromPosition, toPosition); + try { + new Pinned(context).updatePinned(pinned); + } catch (DBException e) { + e.printStackTrace(); + } + return true; + } + + @Override + public int getItemCount() { + return pinned.pinnedTimelines.size(); + } + + /** + * Simple example of a view holder that implements {@link ItemTouchHelperViewHolder} and has a + * "handle" view that initiates a drag event when touched. + */ + public class ReorderViewHolder extends RecyclerView.ViewHolder implements ItemTouchHelperViewHolder { + + DrawerReorderBinding binding; + + ReorderViewHolder(DrawerReorderBinding itemView) { + super(itemView.getRoot()); + binding = itemView; + } + + @Override + public void onItemSelected() { + itemView.setBackgroundColor(ContextCompat.getColor(context, R.color.mastodonC3)); + } + + @Override + public void onItemClear() { + itemView.setBackgroundColor(0); + } + } +} diff --git a/app/src/main/java/app/fedilab/android/ui/drawer/RulesAdapter.java b/app/src/main/java/app/fedilab/android/ui/drawer/RulesAdapter.java new file mode 100644 index 000000000..576e15fef --- /dev/null +++ b/app/src/main/java/app/fedilab/android/ui/drawer/RulesAdapter.java @@ -0,0 +1,82 @@ +package app.fedilab.android.ui.drawer; +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.ArrayList; +import java.util.List; + +import app.fedilab.android.client.mastodon.entities.Instance; +import app.fedilab.android.databinding.DrawerCheckboxBinding; + +public class RulesAdapter extends RecyclerView.Adapter { + private final List ruleList; + + public RulesAdapter(List rules) { + this.ruleList = rules; + } + + public int getCount() { + return ruleList.size(); + } + + public Instance.Rule getItem(int position) { + return ruleList.get(position); + } + + @NonNull + @Override + public RuleViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + DrawerCheckboxBinding itemBinding = DrawerCheckboxBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new RuleViewHolder(itemBinding); + } + + public List getChecked() { + List checkedItems = new ArrayList<>(); + for (Instance.Rule rule : ruleList) { + if (rule.isChecked) { + checkedItems.add(rule.id); + } + } + return checkedItems; + } + + @Override + public void onBindViewHolder(@NonNull RuleViewHolder holder, int position) { + Instance.Rule rule = ruleList.get(position); + holder.binding.checkbox.setText(rule.text); + holder.binding.checkbox.setOnCheckedChangeListener((compoundButton, checked) -> rule.isChecked = checked); + } + + @Override + public int getItemCount() { + return ruleList.size(); + } + + + public static class RuleViewHolder extends RecyclerView.ViewHolder { + DrawerCheckboxBinding binding; + + RuleViewHolder(DrawerCheckboxBinding itemView) { + super(itemView.getRoot()); + binding = itemView; + } + } +} diff --git a/app/src/main/java/app/fedilab/android/ui/drawer/StatusAdapter.java b/app/src/main/java/app/fedilab/android/ui/drawer/StatusAdapter.java new file mode 100644 index 000000000..26ad63f70 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/ui/drawer/StatusAdapter.java @@ -0,0 +1,1435 @@ +package app.fedilab.android.ui.drawer; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + + +import static app.fedilab.android.activities.ContextActivity.displayCW; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.graphics.PorterDuff; +import android.graphics.Typeface; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.os.Bundle; +import android.os.CountDownTimer; +import android.os.Handler; +import android.os.Looper; +import android.text.Html; +import android.text.Layout; +import android.text.SpannableString; +import android.text.Spanned; +import android.text.TextUtils; +import android.text.method.LinkMovementMethod; +import android.text.style.ForegroundColorSpan; +import android.view.ContextThemeWrapper; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CheckBox; +import android.widget.ImageView; +import android.widget.RadioButton; +import android.widget.RelativeLayout; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.PopupMenu; +import androidx.core.app.ActivityOptionsCompat; +import androidx.core.content.ContextCompat; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.ViewModelProvider; +import androidx.lifecycle.ViewModelStoreOwner; +import androidx.preference.PreferenceManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.load.resource.bitmap.CenterCrop; +import com.bumptech.glide.load.resource.bitmap.RoundedCorners; +import com.bumptech.glide.request.RequestOptions; +import com.github.stom79.mytransl.MyTransL; +import com.github.stom79.mytransl.client.HttpsConnectionException; +import com.github.stom79.mytransl.client.Results; +import com.github.stom79.mytransl.translate.Params; +import com.github.stom79.mytransl.translate.Translate; +import com.varunest.sparkbutton.SparkButton; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Timer; +import java.util.TimerTask; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import app.fedilab.android.BaseMainActivity; +import app.fedilab.android.R; +import app.fedilab.android.activities.ComposeActivity; +import app.fedilab.android.activities.ContextActivity; +import app.fedilab.android.activities.CustomSharingActivity; +import app.fedilab.android.activities.MediaActivity; +import app.fedilab.android.activities.ProfileActivity; +import app.fedilab.android.activities.ReportActivity; +import app.fedilab.android.activities.StatusInfoActivity; +import app.fedilab.android.client.entities.StatusDraft; +import app.fedilab.android.client.mastodon.entities.Attachment; +import app.fedilab.android.client.mastodon.entities.Notification; +import app.fedilab.android.client.mastodon.entities.Poll; +import app.fedilab.android.client.mastodon.entities.Status; +import app.fedilab.android.databinding.DrawerStatusBinding; +import app.fedilab.android.databinding.DrawerStatusNotificationBinding; +import app.fedilab.android.databinding.DrawerStatusReportBinding; +import app.fedilab.android.databinding.LayoutMediaBinding; +import app.fedilab.android.databinding.LayoutPollItemBinding; +import app.fedilab.android.helper.CrossActionHelper; +import app.fedilab.android.helper.Helper; +import app.fedilab.android.helper.MastodonHelper; +import app.fedilab.android.helper.SpannableHelper; +import app.fedilab.android.ui.fragment.timeline.FragmentMastodonContext; +import app.fedilab.android.viewmodel.mastodon.AccountsVM; +import app.fedilab.android.viewmodel.mastodon.SearchVM; +import app.fedilab.android.viewmodel.mastodon.StatusesVM; +import es.dmoral.toasty.Toasty; +import jp.wasabeef.glide.transformations.BlurTransformation; + + +public class StatusAdapter extends RecyclerView.Adapter { + private final List statusList; + private final boolean remote; + private final boolean minified; + private Context context; + + public StatusAdapter(List statuses, boolean remote, boolean minified) { + this.statusList = statuses; + this.remote = remote; + this.minified = minified; + } + + + public StatusAdapter(List statuses, boolean remote) { + this.statusList = statuses; + this.remote = remote; + this.minified = false; + } + + + /** + * Manage status, this method is also reused in notifications timelines + * + * @param context Context + * @param statusesVM StatusesVM - For handling actions in background to the correct activity + * @param searchVM SearchVM - For handling remote actions + * @param holder StatusViewHolder + * @param adapter RecyclerView.Adapter - General adapter that can be for {@link StatusAdapter} or {@link NotificationAdapter} + * @param statusList List + * @param notificationList List + * @param remote boolean Indicate if the status is a remote one (ie not yet federated) + * @param status {@link Status} + */ + @SuppressLint("ClickableViewAccessibility") + public static void statusManagement(Context context, + StatusesVM statusesVM, + SearchVM searchVM, + StatusViewHolder holder, + RecyclerView.Adapter adapter, + List statusList, + List notificationList, + Status status, + boolean remote, + boolean minified) { + + Status statusToDeal = status.reblog != null ? status.reblog : status; + + final SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(context); + + boolean expand_cw = sharedpreferences.getBoolean(context.getString(R.string.SET_EXPAND_CW), false); + boolean expand_media = sharedpreferences.getBoolean(context.getString(R.string.SET_EXPAND_MEDIA), false); + boolean display_card = sharedpreferences.getBoolean(context.getString(R.string.SET_DISPLAY_CARD), false); + boolean share_details = sharedpreferences.getBoolean(context.getString(R.string.SET_SHARE_DETAILS), true); + boolean confirmFav = sharedpreferences.getBoolean(context.getString(R.string.SET_NOTIF_VALIDATION_FAV), false); + boolean confirmBoost = sharedpreferences.getBoolean(context.getString(R.string.SET_NOTIF_VALIDATION), true); + boolean fullAttachement = sharedpreferences.getBoolean(context.getString(R.string.SET_FULL_PREVIEW), false); + int truncate_toots_size = sharedpreferences.getInt(context.getString(R.string.SET_TRUNCATE_TOOTS_SIZE), 0); + boolean display_video_preview = sharedpreferences.getBoolean(context.getString(R.string.SET_DISPLAY_VIDEO_PREVIEWS), true); + boolean isModerator = sharedpreferences.getBoolean(Helper.PREF_IS_MODERATOR, false); + boolean isAdmin = sharedpreferences.getBoolean(Helper.PREF_IS_ADMINISTRATOR, false); + int theme_icons_color = -1; + int theme_statuses_color = -1; + int theme_boost_header_color = -1; + int theme_text_color = -1; + int theme_text_header_1_line = -1; + int theme_text_header_2_line = -1; + if (sharedpreferences.getBoolean("use_custom_theme", false)) { + //Getting custom colors + theme_icons_color = sharedpreferences.getInt("theme_icons_color", -1); + theme_statuses_color = sharedpreferences.getInt("theme_statuses_color", -1); + theme_boost_header_color = sharedpreferences.getInt("theme_boost_header_color", -1); + theme_text_color = sharedpreferences.getInt("theme_text_color", -1); + theme_text_header_1_line = sharedpreferences.getInt("theme_text_header_1_line", -1); + theme_text_header_2_line = sharedpreferences.getInt("theme_text_header_2_line", -1); + + } + + + if (theme_icons_color != -1) { + Helper.changeDrawableColor(context, holder.binding.actionButtonReply, theme_icons_color); + Helper.changeDrawableColor(context, holder.binding.actionButtonMore, theme_icons_color); + Helper.changeDrawableColor(context, R.drawable.ic_baseline_star_24, theme_icons_color); + Helper.changeDrawableColor(context, R.drawable.ic_repeat, theme_icons_color); + Helper.changeDrawableColor(context, holder.binding.visibility, theme_icons_color); + Helper.changeDrawableColor(context, R.drawable.ic_star_outline, theme_icons_color); + Helper.changeDrawableColor(context, R.drawable.ic_person, theme_icons_color); + } + + holder.binding.actionButtonFavorite.pressOnTouch(false); + holder.binding.actionButtonBoost.pressOnTouch(false); + holder.binding.actionButtonFavorite.setActiveImage(R.drawable.ic_baseline_star_24); + holder.binding.actionButtonFavorite.setInactiveImage(R.drawable.ic_star_outline); + holder.binding.actionButtonFavorite.setDisableCircle(true); + holder.binding.actionButtonBoost.setDisableCircle(true); + holder.binding.actionButtonFavorite.setActiveImageTint(R.color.marked_icon); + holder.binding.actionButtonBoost.setActiveImageTint(R.color.boost_icon); + holder.binding.actionButtonFavorite.setInActiveImageTintColor(theme_icons_color); + holder.binding.actionButtonBoost.setInActiveImageTintColor(theme_icons_color); + holder.binding.actionButtonBoost.setColors(R.color.marked_icon, R.color.marked_icon); + + if (theme_text_header_2_line != -1) { + Pattern hashAcct; + SpannableString wordToSpan; + if (status.reblog != null) { + wordToSpan = new SpannableString("@" + status.reblog.account.acct); + hashAcct = Pattern.compile("(@" + status.reblog.account.acct + ")"); + } else { + wordToSpan = new SpannableString("@" + status.account.acct); + hashAcct = Pattern.compile("(@" + status.account.acct + ")"); + } + Matcher matcherAcct = hashAcct.matcher(wordToSpan); + while (matcherAcct.find()) { + int matchStart = matcherAcct.start(1); + int matchEnd = matcherAcct.end(); + if (wordToSpan.length() >= matchEnd && matchStart < matchEnd) { + wordToSpan.setSpan(new ForegroundColorSpan(theme_text_header_2_line), matchStart, matchEnd, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + } + } + Helper.changeDrawableColor(context, holder.binding.statusBoostIcon, theme_text_header_2_line); + } + if (theme_statuses_color != -1) { + holder.binding.cardviewContainer.setBackgroundColor(theme_statuses_color); + holder.binding.translationLabel.setBackgroundColor(theme_statuses_color); + } + if (theme_boost_header_color != -1 && status.reblog != null) { + holder.binding.headerContainer.setBackgroundColor(theme_boost_header_color); + } + if (theme_text_color != -1) { + holder.binding.statusContent.setTextColor(theme_text_color); + holder.binding.statusContentTranslated.setTextColor(theme_text_color); + holder.binding.spoiler.setTextColor(theme_text_color); + holder.binding.dateShort.setTextColor(theme_text_color); + holder.binding.poll.pollInfo.setTextColor(theme_text_color); + holder.binding.cardDescription.setTextColor(theme_text_color); + holder.binding.time.setTextColor(theme_text_color); + holder.binding.reblogsCount.setTextColor(theme_text_color); + holder.binding.favoritesCount.setTextColor(theme_text_color); + holder.binding.favoritesCount.setTextColor(theme_text_color); + Helper.changeDrawableColor(context, holder.binding.repeatInfo, theme_text_color); + Helper.changeDrawableColor(context, holder.binding.favInfo, theme_text_color); + } + + + + if (truncate_toots_size > 0) { + holder.binding.statusContent.setMaxLines(truncate_toots_size); + holder.binding.statusContent.setEllipsize(TextUtils.TruncateAt.END); + Layout layout = holder.binding.statusContent.getLayout(); + if (layout != null) { + int lines = layout.getLineCount(); + if (lines > truncate_toots_size) { + int ellipsisCount = layout.getEllipsisCount(lines - 1); + if (ellipsisCount > truncate_toots_size) { + holder.binding.toggleTruncate.setVisibility(View.VISIBLE); + holder.binding.toggleTruncate.setOnClickListener(v -> { + statusToDeal.isTruncated = !statusToDeal.isTruncated; + adapter.notifyItemChanged(getPositionAsync(notificationList, statusList, statusToDeal)); + }); + if (statusToDeal.isTruncated) { + holder.binding.statusContent.setMaxLines(5); + } else { + holder.binding.statusContent.setMaxLines(9999); + } + } + } + } + } + if (status.card != null && (display_card || status.isFocused) & status.card.description.trim().length() > 0) { + if (status.card.width > status.card.height) { + holder.binding.cardImageHorizontal.setVisibility(View.VISIBLE); + holder.binding.cardImageVertical.setVisibility(View.GONE); + Glide.with(context).load(status.card.image).into(holder.binding.cardImageHorizontal); + } else { + holder.binding.cardImageHorizontal.setVisibility(View.GONE); + holder.binding.cardImageVertical.setVisibility(View.VISIBLE); + Glide.with(context).load(status.card.image).into(holder.binding.cardImageVertical); + } + holder.binding.cardTitle.setText(status.card.title); + holder.binding.cardDescription.setText(status.card.description); + holder.binding.cardUrl.setText(status.card.url); + holder.binding.cardUrl.setOnClickListener(v -> Helper.openBrowser(context, status.card.url)); + holder.binding.card.setVisibility(View.VISIBLE); + } else { + holder.binding.card.setVisibility(View.GONE); + } + if (minified) { + holder.binding.actionButtons.setVisibility(View.GONE); + } else { + holder.binding.actionButtons.setVisibility(View.VISIBLE); + //--- ACTIONS --- + holder.binding.actionButtonFavorite.setChecked(statusToDeal.favourited); + holder.binding.statusUserInfo.setOnClickListener(v -> { + if (remote) { + searchVM.search(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, statusToDeal.url, null, "statuses", false, true, false, 0, null, null, 1) + .observe((LifecycleOwner) context, results -> { + if (results.statuses != null && results.statuses.size() > 0) { + Status fetchedStatus = statusList.get(0); + Intent intent = new Intent(context, ProfileActivity.class); + Bundle b = new Bundle(); + b.putSerializable(Helper.ARG_ACCOUNT, fetchedStatus.reblog != null ? fetchedStatus.reblog.account : fetchedStatus.account); + intent.putExtras(b); + ActivityOptionsCompat options = ActivityOptionsCompat + .makeSceneTransitionAnimation((Activity) context, holder.binding.avatar, context.getString(R.string.activity_porfile_pp)); + // start the new activity + context.startActivity(intent, options.toBundle()); + } else { + Toasty.info(context, context.getString(R.string.toast_error_search), Toasty.LENGTH_SHORT).show(); + } + }); + } else { + Intent intent = new Intent(context, ProfileActivity.class); + Bundle b = new Bundle(); + b.putSerializable(Helper.ARG_ACCOUNT, status.reblog != null ? status.reblog.account : status.account); + intent.putExtras(b); + ActivityOptionsCompat options = ActivityOptionsCompat + .makeSceneTransitionAnimation((Activity) context, holder.binding.avatar, context.getString(R.string.activity_porfile_pp)); + // start the new activity + context.startActivity(intent, options.toBundle()); + } + }); + holder.binding.statusBoosterAvatar.setOnClickListener(v -> { + if (remote) { + searchVM.search(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, statusToDeal.url, null, "statuses", false, true, false, 0, null, null, 1) + .observe((LifecycleOwner) context, results -> { + if (results.statuses != null && results.statuses.size() > 0) { + Status fetchedStatus = statusList.get(0); + Intent intent = new Intent(context, ProfileActivity.class); + Bundle b = new Bundle(); + b.putSerializable(Helper.ARG_ACCOUNT, fetchedStatus.account); + intent.putExtras(b); + ActivityOptionsCompat options = ActivityOptionsCompat + .makeSceneTransitionAnimation((Activity) context, holder.binding.statusBoosterAvatar, context.getString(R.string.activity_porfile_pp)); + // start the new activity + context.startActivity(intent, options.toBundle()); + } else { + Toasty.info(context, context.getString(R.string.toast_error_search), Toasty.LENGTH_SHORT).show(); + } + }); + } else { + Intent intent = new Intent(context, ProfileActivity.class); + Bundle b = new Bundle(); + b.putSerializable(Helper.ARG_ACCOUNT, status.account); + intent.putExtras(b); + ActivityOptionsCompat options = ActivityOptionsCompat + .makeSceneTransitionAnimation((Activity) context, holder.binding.statusBoosterAvatar, context.getString(R.string.activity_porfile_pp)); + // start the new activity + context.startActivity(intent, options.toBundle()); + } + }); + //---> REBLOG/UNREBLOG + holder.binding.actionButtonBoost.setOnLongClickListener(v -> { + if (statusToDeal.visibility.equals("direct") || (statusToDeal.visibility.equals("private"))) { + return true; + } + if (confirmBoost) { + AlertDialog.Builder alt_bld = new AlertDialog.Builder(context, Helper.dialogStyle()); + if (statusToDeal.reblogged) { + alt_bld.setMessage(context.getString(R.string.reblog_remove)); + } else { + alt_bld.setMessage(context.getString(R.string.reblog_add)); + } + alt_bld.setPositiveButton(R.string.yes, (dialog, id) -> { + CrossActionHelper.doCrossAction(context, CrossActionHelper.TypeOfCrossAction.REBLOG_ACTION, null, statusToDeal); + dialog.dismiss(); + }); + alt_bld.setNegativeButton(R.string.cancel, (dialog, id) -> dialog.dismiss()); + AlertDialog alert = alt_bld.create(); + alert.show(); + } else { + CrossActionHelper.doCrossAction(context, CrossActionHelper.TypeOfCrossAction.REBLOG_ACTION, null, statusToDeal); + } + return true; + }); + holder.binding.actionButtonBoost.setOnClickListener(v -> { + if (confirmBoost) { + AlertDialog.Builder alt_bld = new AlertDialog.Builder(context, Helper.dialogStyle()); + if (statusToDeal.reblogged) { + alt_bld.setMessage(context.getString(R.string.reblog_remove)); + } else { + alt_bld.setMessage(context.getString(R.string.reblog_add)); + } + alt_bld.setPositiveButton(R.string.yes, (dialog, id) -> { + if (remote) { + searchVM.search(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, statusToDeal.url, null, "statuses", false, true, false, 0, null, null, 1) + .observe((LifecycleOwner) context, results -> { + if (results.statuses != null && results.statuses.size() > 0) { + Status fetchedStatus = statusList.get(0); + statusesVM.reblog(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, fetchedStatus.id, null) + .observe((LifecycleOwner) context, _status -> { + statusToDeal.reblogged = _status.reblogged; + statusToDeal.reblogs_count = _status.reblogs_count; + adapter.notifyItemChanged(getPositionAsync(notificationList, statusList, statusToDeal)); + }); + } else { + Toasty.info(context, context.getString(R.string.toast_error_search), Toasty.LENGTH_SHORT).show(); + } + }); + } else { + if (statusToDeal.reblogged) { + statusesVM.unReblog(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, statusToDeal.id) + .observe((LifecycleOwner) context, _status -> { + statusToDeal.reblogged = _status.reblogged; + statusToDeal.reblogs_count = _status.reblogs_count; + adapter.notifyItemChanged(getPositionAsync(notificationList, statusList, statusToDeal)); + }); + } else { + ((SparkButton) v).playAnimation(); + statusesVM.reblog(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, statusToDeal.id, null) + .observe((LifecycleOwner) context, _status -> { + statusToDeal.reblogged = _status.reblogged; + statusToDeal.reblogs_count = _status.reblogs_count; + adapter.notifyItemChanged(getPositionAsync(notificationList, statusList, statusToDeal)); + }); + } + } + dialog.dismiss(); + }); + alt_bld.setNegativeButton(R.string.cancel, (dialog, id) -> dialog.dismiss()); + AlertDialog alert = alt_bld.create(); + alert.show(); + } else { + if (statusToDeal.reblogged) { + statusesVM.unReblog(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, status.id) + .observe((LifecycleOwner) context, _status -> { + statusToDeal.reblogged = _status.reblogged; + statusToDeal.reblogs_count = _status.reblogs_count; + adapter.notifyItemChanged(getPositionAsync(notificationList, statusList, status)); + }); + } else { + ((SparkButton) v).playAnimation(); + statusesVM.reblog(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, status.id, null) + .observe((LifecycleOwner) context, _status -> { + statusToDeal.reblogged = _status.reblogged; + statusToDeal.reblogs_count = _status.reblogs_count; + adapter.notifyItemChanged(getPositionAsync(notificationList, statusList, status)); + }); + } + } + }); + holder.binding.actionButtonBoost.setChecked(statusToDeal.reblogged); + //---> FAVOURITE/UNFAVOURITE + holder.binding.actionButtonFavorite.setOnLongClickListener(v -> { + if (statusToDeal.visibility.equals("direct") || (statusToDeal.visibility.equals("private"))) { + return true; + } + if (confirmFav) { + AlertDialog.Builder alt_bld = new AlertDialog.Builder(context, Helper.dialogStyle()); + alt_bld.setMessage(context.getString(R.string.favourite_add)); + alt_bld.setPositiveButton(R.string.yes, (dialog, id) -> { + CrossActionHelper.doCrossAction(context, CrossActionHelper.TypeOfCrossAction.FAVOURITE_ACTION, null, statusToDeal); + }); + alt_bld.setNegativeButton(R.string.cancel, (dialog, id) -> dialog.dismiss()); + AlertDialog alert = alt_bld.create(); + alert.show(); + } else { + CrossActionHelper.doCrossAction(context, CrossActionHelper.TypeOfCrossAction.FAVOURITE_ACTION, null, statusToDeal); + } + return true; + }); + holder.binding.actionButtonFavorite.setOnClickListener(v -> { + if (confirmFav) { + AlertDialog.Builder alt_bld = new AlertDialog.Builder(context, Helper.dialogStyle()); + if (status.favourited) { + alt_bld.setMessage(context.getString(R.string.favourite_remove)); + } else { + alt_bld.setMessage(context.getString(R.string.favourite_add)); + } + alt_bld.setPositiveButton(R.string.yes, (dialog, id) -> { + + if (remote) { + searchVM.search(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, statusToDeal.url, null, "statuses", false, true, false, 0, null, null, 1) + .observe((LifecycleOwner) context, results -> { + if (results.statuses != null && results.statuses.size() > 0) { + Status fetchedStatus = statusList.get(0); + statusesVM.favourite(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, fetchedStatus.id) + .observe((LifecycleOwner) context, _status -> { + statusToDeal.favourited = _status.favourited; + statusToDeal.favourites_count = _status.favourites_count; + adapter.notifyItemChanged(getPositionAsync(notificationList, statusList, statusToDeal)); + }); + } else { + Toasty.info(context, context.getString(R.string.toast_error_search), Toasty.LENGTH_SHORT).show(); + } + }); + } else { + if (status.favourited) { + statusesVM.unFavourite(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, statusToDeal.id) + .observe((LifecycleOwner) context, _status -> { + statusToDeal.favourited = _status.favourited; + statusToDeal.favourites_count = _status.favourites_count; + adapter.notifyItemChanged(getPositionAsync(notificationList, statusList, statusToDeal)); + }); + } else { + ((SparkButton) v).playAnimation(); + statusesVM.favourite(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, statusToDeal.id) + .observe((LifecycleOwner) context, _status -> { + statusToDeal.favourited = _status.favourited; + statusToDeal.favourites_count = _status.favourites_count; + adapter.notifyItemChanged(getPositionAsync(notificationList, statusList, statusToDeal)); + }); + } + } + dialog.dismiss(); + }); + alt_bld.setNegativeButton(R.string.cancel, (dialog, id) -> dialog.dismiss()); + AlertDialog alert = alt_bld.create(); + alert.show(); + } else { + if (status.favourited) { + statusesVM.unFavourite(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, statusToDeal.id) + .observe((LifecycleOwner) context, _status -> { + statusToDeal.favourited = _status.favourited; + statusToDeal.favourites_count = _status.favourites_count; + adapter.notifyItemChanged(getPositionAsync(notificationList, statusList, statusToDeal)); + }); + } else { + ((SparkButton) v).playAnimation(); + statusesVM.favourite(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, statusToDeal.id) + .observe((LifecycleOwner) context, _status -> { + statusToDeal.favourited = _status.favourited; + statusToDeal.favourites_count = _status.favourites_count; + adapter.notifyItemChanged(getPositionAsync(notificationList, statusList, statusToDeal)); + }); + } + } + }); + } + + + //--- ACCOUNT INFO --- + MastodonHelper.loadPPMastodon(holder.binding.avatar, statusToDeal.account); + holder.binding.displayName.setText(statusToDeal.account.span_display_name, TextView.BufferType.SPANNABLE); + if (theme_text_header_1_line != -1) { + holder.binding.displayName.setTextColor(theme_text_header_1_line); + } + holder.binding.username.setText(String.format("@%s", statusToDeal.account.acct)); + if (theme_text_header_2_line != -1) { + holder.binding.username.setTextColor(theme_text_header_2_line); + } + if (status.isFocused) { + holder.binding.statusInfo.setVisibility(View.VISIBLE); + holder.binding.reblogsCount.setText(String.valueOf(status.reblogs_count)); + holder.binding.favoritesCount.setText(String.valueOf(status.favourites_count)); + holder.binding.time.setText(Helper.longDateToString(status.created_at)); + holder.binding.time.setVisibility(View.VISIBLE); + holder.binding.dateShort.setVisibility(View.GONE); + int ressource = R.drawable.ic_baseline_public_24; + switch (status.visibility) { + case "unlisted": + ressource = R.drawable.ic_baseline_lock_open_24; + break; + case "private": + ressource = R.drawable.ic_baseline_lock_24; + break; + case "direct": + ressource = R.drawable.ic_baseline_mail_24; + break; + } + holder.binding.visibility.setImageResource(ressource); + holder.binding.dateShort.setVisibility(View.GONE); + } else { + holder.binding.statusInfo.setVisibility(View.GONE); + holder.binding.dateShort.setVisibility(View.VISIBLE); + holder.binding.dateShort.setText(Helper.dateDiff(context, status.created_at)); + holder.binding.time.setVisibility(View.GONE); + } + + //---- SPOILER TEXT ----- + + if (statusToDeal.spoiler_text != null && !statusToDeal.spoiler_text.trim().isEmpty()) { + if (!expand_cw && context instanceof ContextActivity && statusToDeal.sensitive) { + statusToDeal.isExpended = displayCW; + status.isMediaDisplayed = displayCW; + } + holder.binding.spoilerExpand.setOnClickListener(v -> { + statusToDeal.isExpended = !status.isExpended; + statusToDeal.isMediaDisplayed = !status.isMediaDisplayed; + adapter.notifyItemChanged(getPositionAsync(notificationList, statusList, statusToDeal)); + }); + holder.binding.spoilerExpand.setVisibility(View.VISIBLE); + holder.binding.spoiler.setVisibility(View.VISIBLE); + + holder.binding.spoiler.setText(statusToDeal.span_spoiler_text, TextView.BufferType.SPANNABLE); + } else { + holder.binding.spoiler.setVisibility(View.GONE); + holder.binding.spoilerExpand.setVisibility(View.GONE); + holder.binding.spoiler.setText(null); + } + + //--- BOOSTER INFO --- + if (status.reblog != null) { + MastodonHelper.loadPPMastodon(holder.binding.statusBoosterAvatar, status.account); + holder.binding.statusBoosterInfo.setVisibility(View.VISIBLE); + } else { + holder.binding.statusBoosterInfo.setVisibility(View.GONE); + } + //--- BOOST VISIBILITY --- + switch (status.visibility) { + case "public": + case "unlisted": + holder.binding.actionButtonBoost.setVisibility(View.VISIBLE); + break; + case "private": + if (status.account.id.compareTo(BaseMainActivity.currentUserID) == 0) { + holder.binding.actionButtonBoost.setVisibility(View.VISIBLE); + } else { + holder.binding.actionButtonBoost.setVisibility(View.GONE); + } + break; + case "direct": + holder.binding.actionButtonBoost.setVisibility(View.GONE); + break; + } + //--- MAIN CONTENT --- + holder.binding.statusContent.setText(statusToDeal.span_content, TextView.BufferType.SPANNABLE); + + if (statusToDeal.translationContent != null) { + holder.binding.containerTrans.setVisibility(View.VISIBLE); + holder.binding.statusContentTranslated.setText(statusToDeal.span_translate, TextView.BufferType.SPANNABLE); + } else { + holder.binding.containerTrans.setVisibility(View.GONE); + } + + if (status.spoiler_text == null || status.spoiler_text.trim().isEmpty() || statusToDeal.isExpended) { + if (statusToDeal.content.trim().length() == 0) { + holder.binding.mediaContainer.setVisibility(View.GONE); + } else { + holder.binding.statusContent.setVisibility(View.VISIBLE); + + } + } else { + holder.binding.statusContent.setVisibility(View.GONE); + holder.binding.mediaContainer.setVisibility(View.GONE); + } + LayoutInflater inflater = ((Activity) context).getLayoutInflater(); + //--- MEDIA ATTACHMENT --- + if (statusToDeal.media_attachments != null && statusToDeal.media_attachments.size() > 0) { + holder.binding.attachmentsList.removeAllViews(); + holder.binding.mediaContainer.removeAllViews(); + //If only one attachment + if (statusToDeal.media_attachments.size() == 1) { + LayoutMediaBinding layoutMediaBinding = LayoutMediaBinding.inflate(LayoutInflater.from(context), holder.binding.attachmentsList, false); + RelativeLayout.LayoutParams lp; + if (fullAttachement) { + lp = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.WRAP_CONTENT); + layoutMediaBinding.media.setScaleType(ImageView.ScaleType.FIT_CENTER); + } else { + lp = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, (int) Helper.convertDpToPixel(200, context)); + layoutMediaBinding.media.setScaleType(ImageView.ScaleType.CENTER_CROP); + } + if (statusToDeal.sensitive) { + Helper.changeDrawableColor(context, layoutMediaBinding.viewHide, R.color.red_1); + } else { + Helper.changeDrawableColor(context, layoutMediaBinding.viewHide, R.color.white); + } + layoutMediaBinding.media.setLayoutParams(lp); + layoutMediaBinding.media.setOnClickListener(v -> { + if (statusToDeal.isMediaObfuscated && mediaObfuscated(statusToDeal) && !expand_media) { + statusToDeal.isMediaObfuscated = false; + adapter.notifyItemChanged(getPositionAsync(notificationList, statusList, statusToDeal)); + final int timeout = sharedpreferences.getInt(context.getString(R.string.SET_NSFW_TIMEOUT), 5); + if (timeout > 0) { + new CountDownTimer((timeout * 1000L), 1000) { + public void onTick(long millisUntilFinished) { + } + + public void onFinish() { + status.isMediaObfuscated = true; + adapter.notifyItemChanged(getPositionAsync(notificationList, statusList, statusToDeal)); + } + }.start(); + } + return; + } + Intent mediaIntent = new Intent(context, MediaActivity.class); + Bundle b = new Bundle(); + b.putInt(Helper.ARG_MEDIA_POSITION, 1); + b.putSerializable(Helper.ARG_MEDIA_ARRAY, new ArrayList<>(statusToDeal.media_attachments)); + mediaIntent.putExtras(b); + ActivityOptionsCompat options = ActivityOptionsCompat + .makeSceneTransitionAnimation((Activity) context, layoutMediaBinding.media, statusToDeal.media_attachments.get(0).url); + // start the new activity + context.startActivity(mediaIntent, options.toBundle()); + }); + if (!mediaObfuscated(statusToDeal) || expand_media) { + layoutMediaBinding.viewHide.setImageResource(R.drawable.ic_baseline_visibility_24); + Glide.with(layoutMediaBinding.media.getContext()) + .load(statusToDeal.media_attachments.get(0).preview_url) + .apply(new RequestOptions().transform(new CenterCrop(), new RoundedCorners((int) Helper.convertDpToPixel(3, context)))) + .into(layoutMediaBinding.media); + } else { + layoutMediaBinding.viewHide.setImageResource(R.drawable.ic_baseline_visibility_off_24); + Glide.with(layoutMediaBinding.media.getContext()) + .load(statusToDeal.media_attachments.get(0).preview_url) + .apply(new RequestOptions().transform(new BlurTransformation(50, 3))) + // .apply(new RequestOptions().transform(new CenterCrop(), new RoundedCorners((int) Helper.convertDpToPixel(3, context)))) + .into(layoutMediaBinding.media); + } + layoutMediaBinding.viewHide.setOnClickListener(v -> { + statusToDeal.sensitive = !statusToDeal.sensitive; + adapter.notifyItemChanged(getPositionAsync(notificationList, statusList, statusToDeal)); + }); + holder.binding.mediaContainer.addView(layoutMediaBinding.getRoot()); + holder.binding.mediaContainer.setVisibility(View.VISIBLE); + holder.binding.attachmentsListContainer.setVisibility(View.GONE); + } else { //Several media + + for (Attachment attachment : statusToDeal.media_attachments) { + LayoutMediaBinding layoutMediaBinding = LayoutMediaBinding.inflate(LayoutInflater.from(context), holder.binding.attachmentsList, false); + RelativeLayout.LayoutParams lp; + if (fullAttachement) { + lp = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT); + layoutMediaBinding.media.setScaleType(ImageView.ScaleType.FIT_CENTER); + } else { + lp = new RelativeLayout.LayoutParams((int) Helper.convertDpToPixel(200, context), (int) Helper.convertDpToPixel(200, context)); + layoutMediaBinding.media.setScaleType(ImageView.ScaleType.CENTER_CROP); + } + lp.setMargins(0, 0, (int) Helper.convertDpToPixel(5, context), 0); + if (!mediaObfuscated(statusToDeal) || expand_media) { + layoutMediaBinding.viewHide.setImageResource(R.drawable.ic_baseline_visibility_24); + Glide.with(layoutMediaBinding.media.getContext()) + .load(attachment.preview_url) + .apply(new RequestOptions().transform(new CenterCrop(), new RoundedCorners((int) Helper.convertDpToPixel(3, context)))) + .into(layoutMediaBinding.media); + } else { + layoutMediaBinding.viewHide.setImageResource(R.drawable.ic_baseline_visibility_off_24); + Glide.with(layoutMediaBinding.media.getContext()) + .load(attachment.preview_url) + .apply(new RequestOptions().transform(new BlurTransformation(50, 3))) + // .apply(new RequestOptions().transform(new CenterCrop(), new RoundedCorners((int) Helper.convertDpToPixel(3, context)))) + .into(layoutMediaBinding.media); + } + if (statusToDeal.sensitive) { + Helper.changeDrawableColor(context, layoutMediaBinding.viewHide, R.color.red_1); + } else { + Helper.changeDrawableColor(context, layoutMediaBinding.viewHide, R.color.white); + } + layoutMediaBinding.media.setLayoutParams(lp); + layoutMediaBinding.media.setOnClickListener(v -> { + Intent mediaIntent = new Intent(context, MediaActivity.class); + Bundle b = new Bundle(); + b.putInt(Helper.ARG_MEDIA_POSITION, 1); + b.putSerializable(Helper.ARG_MEDIA_ARRAY, new ArrayList<>(statusToDeal.media_attachments)); + mediaIntent.putExtras(b); + ActivityOptionsCompat options = ActivityOptionsCompat + .makeSceneTransitionAnimation((Activity) context, layoutMediaBinding.media, statusToDeal.media_attachments.get(0).url); + // start the new activity + context.startActivity(mediaIntent, options.toBundle()); + }); + layoutMediaBinding.viewHide.setOnClickListener(v -> { + statusToDeal.sensitive = !statusToDeal.sensitive; + adapter.notifyItemChanged(getPositionAsync(notificationList, statusList, statusToDeal)); + }); + holder.binding.attachmentsList.addView(layoutMediaBinding.getRoot()); + } + holder.binding.mediaContainer.setVisibility(View.GONE); + holder.binding.attachmentsListContainer.setVisibility(View.VISIBLE); + } + } else { + holder.binding.mediaContainer.setVisibility(View.GONE); + holder.binding.attachmentsListContainer.setVisibility(View.GONE); + } + holder.binding.statusContent.setMovementMethod(LinkMovementMethod.getInstance()); + + holder.binding.reblogInfo.setOnClickListener(v -> { + if (remote) { + searchVM.search(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, statusToDeal.url, null, "statuses", false, true, false, 0, null, null, 1) + .observe((LifecycleOwner) context, results -> { + if (results.statuses != null && results.statuses.size() > 0) { + Status fetchedStatus = statusList.get(0); + if (fetchedStatus.reblogs_count > 0) { + Intent intent = new Intent(context, StatusInfoActivity.class); + intent.putExtra(Helper.ARG_TYPE_OF_INFO, StatusInfoActivity.typeOfInfo.BOOSTED_BY); + intent.putExtra(Helper.ARG_STATUS, fetchedStatus); + context.startActivity(intent); + } + } else { + Toasty.info(context, context.getString(R.string.toast_error_search), Toasty.LENGTH_SHORT).show(); + } + }); + } else { + if (statusToDeal.reblogs_count > 0) { + Intent intent = new Intent(context, StatusInfoActivity.class); + intent.putExtra(Helper.ARG_TYPE_OF_INFO, StatusInfoActivity.typeOfInfo.BOOSTED_BY); + intent.putExtra(Helper.ARG_STATUS, statusToDeal); + context.startActivity(intent); + } + } + }); + + holder.binding.favouriteInfo.setOnClickListener(v -> { + if (remote) { + searchVM.search(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, statusToDeal.url, null, "statuses", false, true, false, 0, null, null, 1) + .observe((LifecycleOwner) context, results -> { + if (results.statuses != null && results.statuses.size() > 0) { + Status fetchedStatus = statusList.get(0); + if (fetchedStatus.favourites_count > 0) { + Intent intent = new Intent(context, StatusInfoActivity.class); + intent.putExtra(Helper.ARG_TYPE_OF_INFO, StatusInfoActivity.typeOfInfo.LIKED_BY); + intent.putExtra(Helper.ARG_STATUS, fetchedStatus); + context.startActivity(intent); + } + } else { + Toasty.info(context, context.getString(R.string.toast_error_search), Toasty.LENGTH_SHORT).show(); + } + }); + } else { + if (statusToDeal.favourites_count > 0) { + Intent intent = new Intent(context, StatusInfoActivity.class); + intent.putExtra(Helper.ARG_TYPE_OF_INFO, StatusInfoActivity.typeOfInfo.LIKED_BY); + intent.putExtra(Helper.ARG_STATUS, statusToDeal); + context.startActivity(intent); + } + } + }); + + // --- POLL --- + if (statusToDeal.poll != null && statusToDeal.poll.options != null) { + if (statusToDeal.poll.voted || statusToDeal.poll.expired) { + holder.binding.poll.submitVote.setVisibility(View.GONE); + holder.binding.poll.rated.setVisibility(View.VISIBLE); + holder.binding.poll.multipleChoice.setVisibility(View.GONE); + holder.binding.poll.singleChoiceRadioGroup.setVisibility(View.GONE); + int greaterValue = 0; + for (Poll.PollItem pollItem : statusToDeal.poll.options) { + if (pollItem.votes_count > greaterValue) + greaterValue = pollItem.votes_count; + } + holder.binding.poll.rated.removeAllViews(); + List ownvotes = statusToDeal.poll.own_votes; + int j = 0; + for (Poll.PollItem pollItem : statusToDeal.poll.options) { + @NonNull LayoutPollItemBinding pollItemBinding = LayoutPollItemBinding.inflate(inflater, holder.binding.poll.rated, true); + double value = ((double) (pollItem.votes_count * 100) / (double) statusToDeal.poll.voters_count); + pollItemBinding.pollItemPercent.setText(String.format("%s %%", (int) value)); + if (theme_text_color != -1) { + pollItemBinding.pollItemPercent.setTextColor(theme_text_color); + pollItemBinding.pollItemText.setTextColor(theme_text_color); + } + pollItemBinding.pollItemText.setText(pollItem.span_title, TextView.BufferType.SPANNABLE); + pollItemBinding.pollItemValue.setProgress((int) value); + if (pollItem.votes_count == greaterValue) { + pollItemBinding.pollItemPercent.setTypeface(null, Typeface.BOLD); + pollItemBinding.pollItemText.setTypeface(null, Typeface.BOLD); + } + if (ownvotes != null && ownvotes.contains(j)) { + Drawable img = ContextCompat.getDrawable(context, R.drawable.ic_baseline_check_24); + assert img != null; + final float scale = context.getResources().getDisplayMetrics().density; + img.setColorFilter(ContextCompat.getColor(context, R.color.cyanea_accent_reference), PorterDuff.Mode.SRC_IN); + img.setBounds(0, 0, (int) (20 * scale + 0.5f), (int) (20 * scale + 0.5f)); + pollItemBinding.pollItemText.setCompoundDrawables(null, null, img, null); + } + j++; + } + } else { + holder.binding.poll.rated.setVisibility(View.GONE); + holder.binding.poll.submitVote.setVisibility(View.VISIBLE); + if (statusToDeal.poll.multiple) { + if ((holder.binding.poll.multipleChoice).getChildCount() > 0) + (holder.binding.poll.multipleChoice).removeAllViews(); + for (Poll.PollItem pollOption : statusToDeal.poll.options) { + CheckBox cb = new CheckBox(context); + cb.setText(pollOption.span_title, TextView.BufferType.SPANNABLE); + holder.binding.poll.multipleChoice.addView(cb); + } + holder.binding.poll.multipleChoice.setVisibility(View.VISIBLE); + holder.binding.poll.singleChoiceRadioGroup.setVisibility(View.GONE); + } else { + if ((holder.binding.poll.singleChoiceRadioGroup).getChildCount() > 0) + (holder.binding.poll.singleChoiceRadioGroup).removeAllViews(); + for (Poll.PollItem pollOption : statusToDeal.poll.options) { + RadioButton rb = new RadioButton(context); + rb.setText(pollOption.span_title, TextView.BufferType.SPANNABLE); + holder.binding.poll.singleChoiceRadioGroup.addView(rb); + } + holder.binding.poll.singleChoiceRadioGroup.setVisibility(View.VISIBLE); + holder.binding.poll.multipleChoice.setVisibility(View.GONE); + } + holder.binding.poll.submitVote.setVisibility(View.VISIBLE); + holder.binding.poll.submitVote.setOnClickListener(v -> { + int[] choice; + if (statusToDeal.poll.multiple) { + ArrayList choices = new ArrayList<>(); + int choicesCount = holder.binding.poll.multipleChoice.getChildCount(); + for (int i1 = 0; i1 < choicesCount; i1++) { + if (holder.binding.poll.multipleChoice.getChildAt(i1) != null && holder.binding.poll.multipleChoice.getChildAt(i1) instanceof CheckBox) { + if (((CheckBox) holder.binding.poll.multipleChoice.getChildAt(i1)).isChecked()) { + choices.add(i1); + } + } + } + choice = new int[choices.size()]; + Iterator iterator = choices.iterator(); + for (int i1 = 0; i1 < choice.length; i1++) { + choice[i1] = iterator.next(); + } + if (choice.length == 0) + return; + } else { + choice = new int[1]; + choice[0] = -1; + int choicesCount = holder.binding.poll.singleChoiceRadioGroup.getChildCount(); + for (int i1 = 0; i1 < choicesCount; i1++) { + if (holder.binding.poll.singleChoiceRadioGroup.getChildAt(i1) != null && holder.binding.poll.singleChoiceRadioGroup.getChildAt(i1) instanceof RadioButton) { + if (((RadioButton) holder.binding.poll.singleChoiceRadioGroup.getChildAt(i1)).isChecked()) { + choice[0] = i1; + } + } + } + if (choice[0] == -1) + return; + } + //Vote on the poll + if (remote) { + searchVM.search(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, statusToDeal.url, null, "statuses", false, true, false, 0, null, null, 1) + .observe((LifecycleOwner) context, results -> { + if (results.statuses != null && results.statuses.size() > 0) { + Status fetchedStatus = statusList.get(0); + statusesVM.votePoll(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, fetchedStatus.poll.id, choice) + .observe((LifecycleOwner) context, poll -> { + int i = 0; + for (Poll.PollItem item : statusToDeal.poll.options) { + poll.options.get(i).span_title = item.span_title; + i++; + } + statusToDeal.poll = poll; + adapter.notifyItemChanged(getPositionAsync(notificationList, statusList, statusToDeal)); + }); + } else { + Toasty.info(context, context.getString(R.string.toast_error_search), Toasty.LENGTH_SHORT).show(); + } + }); + } else { + statusesVM.votePoll(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, statusToDeal.poll.id, choice) + .observe((LifecycleOwner) context, poll -> { + if (poll != null) { + int i = 0; + for (Poll.PollItem item : statusToDeal.poll.options) { + poll.options.get(i).span_title = item.span_title; + i++; + } + statusToDeal.poll = poll; + adapter.notifyItemChanged(getPositionAsync(notificationList, statusList, statusToDeal)); + } + }); + } + }); + } + holder.binding.poll.refreshPoll.setOnClickListener(v -> { + statusesVM.getPoll(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, statusToDeal.poll.id) + .observe((LifecycleOwner) context, poll -> { + //Store span elements + int i = 0; + for (Poll.PollItem item : statusToDeal.poll.options) { + poll.options.get(i).span_title = item.span_title; + i++; + } + statusToDeal.poll = poll; + adapter.notifyItemChanged(getPositionAsync(notificationList, statusList, statusToDeal)); + }); + }); + holder.binding.poll.pollContainer.setVisibility(View.VISIBLE); + String pollInfo = context.getResources().getQuantityString(R.plurals.number_of_voters, statusToDeal.poll.voters_count, statusToDeal.poll.voters_count); + if (statusToDeal.poll.expired) { + pollInfo += " - " + context.getString(R.string.poll_finish_at, MastodonHelper.dateToStringPoll(statusToDeal.poll.expires_at)); + } else { + pollInfo += " - " + context.getString(R.string.poll_finish_in, MastodonHelper.dateDiffPoll(context, statusToDeal.poll.expires_at)); + } + holder.binding.poll.pollInfo.setText(pollInfo); + } else { + holder.binding.poll.pollContainer.setVisibility(View.GONE); + } + holder.binding.statusContent.setOnTouchListener((view, motionEvent) -> { + if (motionEvent.getAction() == MotionEvent.ACTION_UP && !view.hasFocus()) { + try { + view.requestFocus(); + } catch (Exception ignored) { + } + } + return false; + }); + if (!minified) { + holder.binding.statusContent.setOnClickListener(v -> { + if (status.isFocused || v.getTag() == SpannableHelper.CLICKABLE_SPAN) { + if (v.getTag() == SpannableHelper.CLICKABLE_SPAN) { + v.setTag(null); + } + return; + } + if (context instanceof ContextActivity) { + Bundle bundle = new Bundle(); + bundle.putSerializable(Helper.ARG_STATUS, statusToDeal); + Fragment fragment = Helper.addFragment(((AppCompatActivity) context).getSupportFragmentManager(), R.id.nav_host_fragment_content_main, new FragmentMastodonContext(), bundle, null, FragmentMastodonContext.class.getName()); + ((ContextActivity) context).setCurrentFragment((FragmentMastodonContext) fragment); + } else { + if (remote) { + searchVM.search(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, statusToDeal.url, null, "statuses", false, true, false, 0, null, null, 1) + .observe((LifecycleOwner) context, results -> { + if (results.statuses != null && results.statuses.size() > 0) { + Status fetchedStatus = statusList.get(0); + Intent intent = new Intent(context, ContextActivity.class); + intent.putExtra(Helper.ARG_STATUS, fetchedStatus); + context.startActivity(intent); + } else { + Toasty.info(context, context.getString(R.string.toast_error_search), Toasty.LENGTH_SHORT).show(); + } + }); + } else { + Intent intent = new Intent(context, ContextActivity.class); + intent.putExtra(Helper.ARG_STATUS, statusToDeal); + context.startActivity(intent); + } + } + }); + } + + + // Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate(() -> holder.binding.statusContent.invalidate(), 0, 100, TimeUnit.MILLISECONDS); + if (remote) { + holder.binding.actionButtonMore.setVisibility(View.GONE); + } else { + holder.binding.actionButtonMore.setVisibility(View.VISIBLE); + } + holder.binding.actionButtonMore.setOnClickListener(v -> { + boolean isOwner = statusToDeal.account.id.compareTo(BaseMainActivity.currentUserID) == 0; + PopupMenu popup = new PopupMenu(new ContextThemeWrapper(context, Helper.popupStyle()), holder.binding.actionButtonMore); + popup.getMenuInflater() + .inflate(R.menu.option_toot, popup.getMenu()); + if (statusToDeal.visibility.equals("private") || status.visibility.equals("direct")) { + popup.getMenu().findItem(R.id.action_mention).setVisible(false); + } + if (statusToDeal.bookmarked) + popup.getMenu().findItem(R.id.action_bookmark).setTitle(R.string.bookmark_remove); + else + popup.getMenu().findItem(R.id.action_bookmark).setTitle(R.string.bookmark_add); + if (statusToDeal.muted) + popup.getMenu().findItem(R.id.action_mute_conversation).setTitle(R.string.unmute_conversation); + else + popup.getMenu().findItem(R.id.action_mute_conversation).setTitle(R.string.mute_conversation); + + final String[] stringArrayConf; + if (statusToDeal.visibility.equals("direct") || (statusToDeal.visibility.equals("private") && !isOwner)) + popup.getMenu().findItem(R.id.action_schedule_boost).setVisible(false); + if (isOwner) { + popup.getMenu().findItem(R.id.action_block).setVisible(false); + popup.getMenu().findItem(R.id.action_mute).setVisible(false); + popup.getMenu().findItem(R.id.action_report).setVisible(false); + popup.getMenu().findItem(R.id.action_timed_mute).setVisible(false); + popup.getMenu().findItem(R.id.action_block_domain).setVisible(false); + stringArrayConf = context.getResources().getStringArray(R.array.more_action_owner_confirm); + } else { + popup.getMenu().findItem(R.id.action_redraft).setVisible(false); + popup.getMenu().findItem(R.id.action_remove).setVisible(false); + if (statusToDeal.account.acct.split("@").length < 2) + popup.getMenu().findItem(R.id.action_block_domain).setVisible(false); + stringArrayConf = context.getResources().getStringArray(R.array.more_action_confirm); + } + boolean display_admin_statuses = sharedpreferences.getBoolean(context.getString(R.string.SET_DISPLAY_ADMIN_STATUSES) + BaseMainActivity.currentUserID + BaseMainActivity.currentInstance, false); + if (!display_admin_statuses) { + popup.getMenu().findItem(R.id.action_admin).setVisible(false); + } + + boolean custom_sharing = sharedpreferences.getBoolean(context.getString(R.string.SET_CUSTOM_SHARING), false); + if (custom_sharing && statusToDeal.visibility.equals("public")) + popup.getMenu().findItem(R.id.action_custom_sharing).setVisible(true); + AccountsVM accountsVM = new ViewModelProvider((ViewModelStoreOwner) context).get(AccountsVM.class); + popup.setOnMenuItemClickListener(item -> { + int itemId = item.getItemId(); + if (itemId == R.id.action_redraft) { + AlertDialog.Builder builderInner = new AlertDialog.Builder(context, Helper.dialogStyle()); + builderInner.setTitle(stringArrayConf[1]); + builderInner.setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss()); + builderInner.setPositiveButton(R.string.yes, (dialog, which) -> { + if (statusList != null) { + int position = getPositionAsync(notificationList, statusList, statusToDeal); + statusList.remove(statusToDeal); + adapter.notifyItemRemoved(position); + statusesVM.deleteStatus(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, statusToDeal.id).observe((LifecycleOwner) context, statusDeleted -> { + Intent intent = new Intent(context, ComposeActivity.class); + StatusDraft statusDraft = new StatusDraft(); + statusDraft.statusDraftList = new ArrayList<>(); + statusDraft.statusReplyList = new ArrayList<>(); + statusDraft.statusDraftList.add(statusDeleted); + intent.putExtra(Helper.ARG_STATUS_DRAFT, statusDraft); + context.startActivity(intent); + }); + } + }); + builderInner.setMessage(statusToDeal.text); + builderInner.show(); + } else if (itemId == R.id.action_schedule_boost) { + MastodonHelper.scheduleBoost(context, MastodonHelper.ScheduleType.BOOST, statusToDeal, null, null); + } else if (itemId == R.id.action_admin) { + /* Intent intent = new Intent(context, AccountReportActivity.class); + intent.putExtra(Helper.ARG_ACCOUNT, statusToDeal.account); + context.startActivity(intent);*/ + } else if (itemId == R.id.action_open_browser) { + Helper.openBrowser(context, statusToDeal.url); + } else if (itemId == R.id.action_remove) { + AlertDialog.Builder builderInner = new AlertDialog.Builder(context, Helper.dialogStyle()); + builderInner.setTitle(stringArrayConf[0]); + builderInner.setMessage(statusToDeal.text); + builderInner.setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss()); + builderInner.setPositiveButton(R.string.yes, (dialog, which) -> { + statusesVM.deleteStatus(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, statusToDeal.id) + .observe((LifecycleOwner) context, statusDeleted -> { + statusList.remove(statusToDeal); + int position = getPositionAsync(notificationList, statusList, status); + adapter.notifyItemRemoved(position); + }); + }); + builderInner.show(); + } else if (itemId == R.id.action_block_domain) { + AlertDialog.Builder builderInner = new AlertDialog.Builder(context, Helper.dialogStyle()); + builderInner.setTitle(stringArrayConf[3]); + String domain = statusToDeal.account.acct.split("@")[1]; + builderInner.setMessage(context.getString(R.string.block_domain_confirm_message, domain)); + builderInner.setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss()); + builderInner.setPositiveButton(R.string.yes, (dialog, which) -> { + accountsVM.addDomainBlocks(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, domain); + Toasty.info(context, context.getString(R.string.toast_block_domain), Toasty.LENGTH_LONG).show(); + }); + builderInner.show(); + } else if (itemId == R.id.action_mute) { + AlertDialog.Builder builderInner = new AlertDialog.Builder(context, Helper.dialogStyle()); + builderInner.setTitle(stringArrayConf[0]); + builderInner.setMessage(statusToDeal.account.acct); + builderInner.setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss()); + builderInner.setPositiveButton(R.string.yes, (dialog, which) -> { + accountsVM.mute(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, statusToDeal.account.id, null, null) + .observe((LifecycleOwner) context, relationShip -> { + Toasty.info(context, context.getString(R.string.toast_mute), Toasty.LENGTH_LONG).show(); + }); + }); + builderInner.show(); + + } else if (itemId == R.id.action_mute_conversation) { + if (statusToDeal.muted) { + statusesVM.unMute(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, statusToDeal.id).observe((LifecycleOwner) context, status1 -> { + Toasty.info(context, context.getString(R.string.toast_unmute_conversation)).show(); + }); + } else { + statusesVM.mute(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, statusToDeal.id).observe((LifecycleOwner) context, status1 -> { + Toasty.info(context, context.getString(R.string.toast_mute_conversation)).show(); + }); + } + + return true; + } else if (itemId == R.id.action_bookmark) { + if (statusToDeal.bookmarked) { + statusesVM.unBookmark(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, statusToDeal.id).observe((LifecycleOwner) context, status1 -> { + Toasty.info(context, context.getString(R.string.status_unbookmarked)).show(); + }); + } else { + statusesVM.bookmark(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, statusToDeal.id).observe((LifecycleOwner) context, status1 -> { + Toasty.info(context, context.getString(R.string.status_bookmarked)).show(); + }); + } + } else if (itemId == R.id.action_timed_mute) { + MastodonHelper.scheduleBoost(context, MastodonHelper.ScheduleType.TIMED_MUTED, statusToDeal, null, null); + return true; + } else if (itemId == R.id.action_block) { + AlertDialog.Builder builderInner = new AlertDialog.Builder(context, Helper.dialogStyle()); + builderInner.setTitle(stringArrayConf[1]); + builderInner.setMessage(statusToDeal.account.acct); + builderInner.setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss()); + builderInner.setPositiveButton(R.string.yes, (dialog, which) -> { + accountsVM.block(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, statusToDeal.account.id) + .observe((LifecycleOwner) context, relationShip -> { + Toasty.info(context, context.getString(R.string.toast_block)).show(); + }); + }); + builderInner.show(); + } else if (itemId == R.id.action_translate) { + MyTransL.translatorEngine et = MyTransL.translatorEngine.LIBRETRANSLATE; + final MyTransL myTransL = MyTransL.getInstance(et); + myTransL.setObfuscation(true); + Params params = new Params(); + params.setSplit_sentences(false); + params.setFormat(Params.fType.TEXT); + params.setSource_lang("auto"); + myTransL.setLibretranslateDomain("translate.fedilab.app"); + String statusToTranslate; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + statusToTranslate = Html.fromHtml(statusToDeal.content, Html.FROM_HTML_MODE_LEGACY).toString(); + else + statusToTranslate = Html.fromHtml(statusToDeal.content).toString(); + myTransL.translate(statusToTranslate, MyTransL.getLocale(), params, new Results() { + @Override + public void onSuccess(Translate translate) { + if (translate.getTranslatedContent() != null) { + statusToDeal.translationShown = true; + statusToDeal.translationContent = translate.getTranslatedContent(); + new Thread(() -> { + SpannableHelper.convertStatus(context.getApplicationContext(), statusToDeal); + Handler mainHandler = new Handler(Looper.getMainLooper()); + Runnable myRunnable = () -> adapter.notifyItemChanged(getPositionAsync(notificationList, statusList, statusToDeal)); + mainHandler.post(myRunnable); + }).start(); + } else { + Toasty.error(context, context.getString(R.string.toast_error_translate), Toast.LENGTH_LONG).show(); + } + } + + @Override + public void onFail(HttpsConnectionException httpsConnectionException) { + + } + }); + return true; + } else if (itemId == R.id.action_report) { + Intent intent = new Intent(context, ReportActivity.class); + intent.putExtra(Helper.ARG_STATUS, statusToDeal); + context.startActivity(intent); + } else if (itemId == R.id.action_copy) { + ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clip = ClipData.newPlainText(Helper.CLIP_BOARD, status.text); + if (clipboard != null) { + clipboard.setPrimaryClip(clip); + Toasty.info(context, context.getString(R.string.clipboard), Toast.LENGTH_LONG).show(); + } + return true; + } else if (itemId == R.id.action_copy_link) { + ClipboardManager clipboard; + ClipData clip; + clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); + + clip = ClipData.newPlainText(Helper.CLIP_BOARD, statusToDeal.url); + if (clipboard != null) { + clipboard.setPrimaryClip(clip); + Toasty.info(context, context.getString(R.string.clipboard_url), Toast.LENGTH_LONG).show(); + } + return true; + } else if (itemId == R.id.action_share) { + Intent sendIntent = new Intent(Intent.ACTION_SEND); + sendIntent.putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.shared_via)); + String url; + + if (statusToDeal.uri.startsWith("http")) + url = status.uri; + else + url = status.url; + String extra_text; + if (share_details) { + extra_text = statusToDeal.account.acct; + if (extra_text.split("@").length == 1) + extra_text = "@" + extra_text + "@" + BaseMainActivity.currentInstance; + else + extra_text = "@" + extra_text; + extra_text += " \uD83D\uDD17 " + url + "\r\n-\n"; + extra_text += statusToDeal.text; + } else { + extra_text = url; + } + sendIntent.putExtra(Intent.EXTRA_TEXT, extra_text); + sendIntent.setType("text/plain"); + context.startActivity(Intent.createChooser(sendIntent, context.getString(R.string.share_with))); + } else if (itemId == R.id.action_custom_sharing) { + Intent intent = new Intent(context, CustomSharingActivity.class); + intent.putExtra(Helper.ARG_STATUS, statusToDeal); + context.startActivity(intent); + } else if (itemId == R.id.action_mention) { + Intent intent = new Intent(context, ComposeActivity.class); + Bundle b = new Bundle(); + b.putSerializable(Helper.ARG_STATUS_MENTION, statusToDeal); + intent.putExtras(b); + context.startActivity(intent); + } + return true; + }); + popup.show(); + }); + holder.binding.actionButtonReply.setOnClickListener(v -> { + Intent intent = new Intent(context, ComposeActivity.class); + intent.putExtra(Helper.ARG_STATUS_REPLY, statusToDeal); + context.startActivity(intent); + }); + //For reports + + if (holder.bindingReport != null) { + holder.bindingReport.checkbox.setChecked(status.isChecked); + holder.bindingReport.checkbox.setOnClickListener(v -> { + status.isChecked = !status.isChecked; + }); + } + } + + private static boolean mediaObfuscated(Status status) { + //Media is not sensitive and doesn't have a spoiler text + if (!status.isMediaObfuscated) { + return false; + } + if (!status.sensitive && (status.spoiler_text == null || status.spoiler_text.trim().isEmpty())) { + return false; + } + if (status.isMediaObfuscated && status.spoiler_text != null && !status.spoiler_text.trim().isEmpty()) { + return true; + } else { + return status.sensitive; + } + } + + /** + * Will manage the current position of the element in the adapter. Action is async, and position might have changed + * + * @param notificationList List - Not null when calling from notification adapter + * @param statusList ist statusList - Not null when calling from status adapter + * @param status Status - Current status + * @return int - position in real time + */ + private static int getPositionAsync(List notificationList, List statusList, Status status) { + int position = 0; + if (statusList != null) { + for (Status _status : statusList) { + if (_status.id.compareTo(status.id) == 0) { + break; + } + position++; + } + } else if (notificationList != null) { + for (Notification notification : notificationList) { + if (notification.status != null && notification.status.id.compareTo(status.id) == 0) { + break; + } + position++; + } + } + return position; + } + + @NonNull + @Override + public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + context = parent.getContext(); + if (!minified) { + DrawerStatusBinding itemBinding = DrawerStatusBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new StatusViewHolder(itemBinding); + } else { + DrawerStatusReportBinding itemBinding = DrawerStatusReportBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new StatusViewHolder(itemBinding); + } + } + + public int getCount() { + return statusList.size(); + } + + public Status getItem(int position) { + return statusList.get(position); + } + + + public long getItemId(int position) { + return position; + } + + @Override + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) { + Status status = statusList.get(position); + StatusViewHolder holder = (StatusViewHolder) viewHolder; + StatusesVM statusesVM = new ViewModelProvider((ViewModelStoreOwner) context).get(StatusesVM.class); + SearchVM searchVM = new ViewModelProvider((ViewModelStoreOwner) context).get(SearchVM.class); + statusManagement(context, statusesVM, searchVM, holder, this, statusList, null, status, remote, minified); + if (holder.timer != null) { + holder.timer.cancel(); + holder.timer = null; + } + if (status.emojis != null && status.emojis.size() > 0) { + holder.timer = new Timer(); + holder.timer.scheduleAtFixedRate(new TimerTask() { + @Override + public void run() { + holder.binding.statusContent.invalidate(); + } + }, 100, 100); + } + } + + @Override + public int getItemCount() { + return statusList.size(); + } + + @Override + public void onViewRecycled(@NonNull RecyclerView.ViewHolder holder) { + super.onViewRecycled(holder); + if (holder instanceof StatusViewHolder && ((StatusViewHolder) holder).timer != null) { + ((StatusViewHolder) holder).timer.cancel(); + } + } + + public static class StatusViewHolder extends RecyclerView.ViewHolder { + DrawerStatusBinding binding; + DrawerStatusReportBinding bindingReport; + DrawerStatusNotificationBinding bindingNotification; + Timer timer; + + StatusViewHolder(DrawerStatusBinding itemView) { + super(itemView.getRoot()); + binding = itemView; + } + + StatusViewHolder(DrawerStatusReportBinding itemView) { + super(itemView.getRoot()); + bindingReport = itemView; + binding = itemView.status; + } + + StatusViewHolder(DrawerStatusNotificationBinding itemView) { + super(itemView.getRoot()); + bindingNotification = itemView; + binding = itemView.status; + } + + } + + +} \ No newline at end of file diff --git a/app/src/main/java/app/fedilab/android/ui/drawer/StatusDraftAdapter.java b/app/src/main/java/app/fedilab/android/ui/drawer/StatusDraftAdapter.java new file mode 100644 index 000000000..95fa83416 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/ui/drawer/StatusDraftAdapter.java @@ -0,0 +1,172 @@ +package app.fedilab.android.ui.drawer; +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + + +import android.content.Context; +import android.content.Intent; +import android.os.Handler; +import android.os.Looper; +import android.view.LayoutInflater; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import androidx.recyclerview.widget.RecyclerView; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +import app.fedilab.android.R; +import app.fedilab.android.activities.ComposeActivity; +import app.fedilab.android.client.entities.StatusDraft; +import app.fedilab.android.client.mastodon.entities.Attachment; +import app.fedilab.android.client.mastodon.entities.Status; +import app.fedilab.android.databinding.DrawerStatusDraftBinding; +import app.fedilab.android.exception.DBException; +import app.fedilab.android.helper.Helper; + + +public class StatusDraftAdapter extends RecyclerView.Adapter { + private final List statusDrafts; + public DraftActions draftActions; + private Context context; + + public StatusDraftAdapter(List statusDrafts) { + this.statusDrafts = statusDrafts; + } + + public int getCount() { + return statusDrafts.size(); + } + + public StatusDraft getItem(int position) { + return statusDrafts.get(position); + } + + @NonNull + @Override + public StatusDraftHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + context = parent.getContext(); + DrawerStatusDraftBinding itemBinding = DrawerStatusDraftBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new StatusDraftHolder(itemBinding); + } + + @Override + public void onBindViewHolder(@NonNull StatusDraftHolder holder, int position) { + StatusDraft statusDraft = statusDrafts.get(position); + + //--- MAIN CONTENT --- + if (statusDraft.statusDraftList != null && statusDraft.statusDraftList.size() > 0) { + holder.binding.statusContent.setText(statusDraft.statusDraftList.get(0).text, TextView.BufferType.SPANNABLE); + holder.binding.numberOfMessages.setText(String.valueOf(statusDraft.statusDraftList.size())); + int numberOfMedia = 0; + for (Status status : statusDraft.statusDraftList) { + numberOfMedia += status.media_attachments != null ? status.media_attachments.size() : 0; + } + holder.binding.numberOfMessages.setText(String.valueOf(statusDraft.statusDraftList.size())); + holder.binding.numberOfMedia.setText(String.valueOf(numberOfMedia)); + } else { + holder.binding.statusContent.setText(""); + holder.binding.numberOfMessages.setText("0"); + holder.binding.numberOfMessages.setText("0"); + holder.binding.numberOfMedia.setText("0"); + } + //--- DATE --- + holder.binding.date.setText(Helper.dateDiff(context, statusDraft.created_ad)); + + holder.binding.container.setOnClickListener(v -> { + Intent intent = new Intent(context, ComposeActivity.class); + intent.putExtra(Helper.ARG_STATUS_DRAFT, statusDraft); + context.startActivity(intent); + }); + + + holder.binding.delete.setOnClickListener(v -> { + AlertDialog.Builder unfollowConfirm = new AlertDialog.Builder(context, Helper.dialogStyle()); + unfollowConfirm.setMessage(context.getString(R.string.remove_draft)); + unfollowConfirm.setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss()); + unfollowConfirm.setPositiveButton(R.string.delete, (dialog, which) -> { + new Thread(() -> { + try { + //Check if there are media in the drafts + List attachments = new ArrayList<>(); + if (statusDraft.statusDraftList != null) { + for (Status drafts : statusDraft.statusDraftList) { + if (drafts.media_attachments != null && drafts.media_attachments.size() > 0) { + attachments.addAll(drafts.media_attachments); + } + } + } + //If there are media, we need to remove them first. + if (attachments.size() > 0) { + for (Attachment attachment : attachments) { + if (attachment.local_path != null) { + File fileToDelete = new File(attachment.local_path); + if (fileToDelete.exists()) { + //noinspection ResultOfMethodCallIgnored + fileToDelete.delete(); + } + } + } + } + //Delete the draft + new StatusDraft(context).removeDraft(statusDraft); + Handler mainHandler = new Handler(Looper.getMainLooper()); + Runnable myRunnable = () -> { + statusDrafts.remove(statusDraft); + notifyItemRemoved(position); + if (statusDrafts.size() == 0) { + draftActions.onAllDeleted(); + } + }; + mainHandler.post(myRunnable); + } catch (DBException e) { + e.printStackTrace(); + } + }).start(); + dialog.dismiss(); + }); + unfollowConfirm.show(); + }); + + } + + public long getItemId(int position) { + return position; + } + + @Override + public int getItemCount() { + return statusDrafts.size(); + } + + public interface DraftActions { + void onAllDeleted(); + } + + static class StatusDraftHolder extends RecyclerView.ViewHolder { + DrawerStatusDraftBinding binding; + + StatusDraftHolder(DrawerStatusDraftBinding itemView) { + super(itemView.getRoot()); + binding = itemView; + } + } + + +} \ No newline at end of file diff --git a/app/src/main/java/app/fedilab/android/ui/drawer/StatusScheduledAdapter.java b/app/src/main/java/app/fedilab/android/ui/drawer/StatusScheduledAdapter.java new file mode 100644 index 000000000..3518977f5 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/ui/drawer/StatusScheduledAdapter.java @@ -0,0 +1,215 @@ +package app.fedilab.android.ui.drawer; +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + + +import static androidx.core.text.HtmlCompat.FROM_HTML_MODE_LEGACY; + +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import android.text.Html; +import android.text.SpannableString; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.ViewModelProvider; +import androidx.lifecycle.ViewModelStoreOwner; +import androidx.recyclerview.widget.RecyclerView; +import androidx.work.WorkManager; + +import java.util.List; + +import app.fedilab.android.BaseMainActivity; +import app.fedilab.android.R; +import app.fedilab.android.activities.ComposeActivity; +import app.fedilab.android.client.entities.ScheduledBoost; +import app.fedilab.android.client.entities.StatusDraft; +import app.fedilab.android.client.mastodon.entities.ScheduledStatus; +import app.fedilab.android.databinding.DrawerStatusScheduledBinding; +import app.fedilab.android.exception.DBException; +import app.fedilab.android.helper.Helper; +import app.fedilab.android.viewmodel.mastodon.StatusesVM; + + +public class StatusScheduledAdapter extends RecyclerView.Adapter { + private final List scheduledStatuses; + private final List statusDraftList; + private final List scheduledBoosts; + public ScheduledActions scheduledActions; + private Context context; + private ScheduledStatus scheduledStatus; + private StatusDraft statusDraft; + private ScheduledBoost scheduledBoost; + + public StatusScheduledAdapter(List scheduledStatuses, List statusDraftList, List scheduledBoosts) { + this.scheduledStatuses = scheduledStatuses; + this.statusDraftList = statusDraftList; + this.scheduledBoosts = scheduledBoosts; + } + + @NonNull + @Override + public StatusScheduledHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + context = parent.getContext(); + DrawerStatusScheduledBinding itemBinding = DrawerStatusScheduledBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new StatusScheduledHolder(itemBinding); + } + + @Override + public void onBindViewHolder(@NonNull StatusScheduledHolder holder, int position) { + + scheduledStatus = null; + statusDraft = null; + String scheduledDate = null; + String statusContent = null; + if (scheduledStatuses != null) { + scheduledStatus = scheduledStatuses.get(position); + scheduledDate = Helper.dateToString(scheduledStatus.scheduled_at); + statusContent = scheduledStatus.params.text; + if (scheduledStatus.params.in_reply_to_id != null) { + holder.binding.reply.setVisibility(View.VISIBLE); + } else { + holder.binding.reply.setVisibility(View.GONE); + } + } else if (statusDraftList != null) { + statusDraft = statusDraftList.get(position); + scheduledDate = Helper.dateToString(statusDraft.scheduled_at); + statusContent = statusDraft.statusDraftList.get(0).text; + if (statusDraft.statusDraftList.get(0).in_reply_to_id != null) { + holder.binding.reply.setVisibility(View.VISIBLE); + } else { + holder.binding.reply.setVisibility(View.GONE); + } + } else if (scheduledBoosts != null) { + scheduledBoost = scheduledBoosts.get(position); + scheduledDate = Helper.dateToString(scheduledBoost.scheduledAt); + if (scheduledBoost.status.in_reply_to_id != null) { + holder.binding.reply.setVisibility(View.VISIBLE); + } else { + holder.binding.reply.setVisibility(View.GONE); + } + } + + holder.binding.date.setText(scheduledDate); + + if (scheduledBoost != null) { + SpannableString statusContentSpan; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + statusContentSpan = new SpannableString(Html.fromHtml(scheduledBoost.status.content, FROM_HTML_MODE_LEGACY)); + else + statusContentSpan = new SpannableString(Html.fromHtml(statusContent)); + holder.binding.statusContent.setText(statusContentSpan, TextView.BufferType.SPANNABLE); + } else { + holder.binding.statusContent.setText(statusContent); + } + + holder.binding.container.setOnClickListener(v -> { + if (statusDraft != null) { + Intent intent = new Intent(context, ComposeActivity.class); + intent.putExtra(Helper.ARG_STATUS_DRAFT, statusDraft); + context.startActivity(intent); + } + + }); + holder.binding.delete.setOnClickListener(v -> { + AlertDialog.Builder unfollowConfirm = new AlertDialog.Builder(context, Helper.dialogStyle()); + unfollowConfirm.setMessage(context.getString(R.string.remove_scheduled)); + unfollowConfirm.setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss()); + unfollowConfirm.setPositiveButton(R.string.delete, (dialog, which) -> { + if (scheduledStatus != null) { + StatusesVM statusesVM = new ViewModelProvider((ViewModelStoreOwner) context).get(StatusesVM.class); + statusesVM.deleteScheduledStatus(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, scheduledStatus.id) + .observe((LifecycleOwner) context, unused -> { + if (scheduledStatuses != null) { + scheduledStatuses.remove(scheduledStatus); + if (scheduledStatuses.size() == 0) { + scheduledActions.onAllDeleted(); + } + notifyItemRemoved(position); + } + }); + } else if (statusDraft != null) { + try { + new StatusDraft(context).removeScheduled(statusDraft); + WorkManager.getInstance(context).cancelWorkById(statusDraft.workerUuid); + if (statusDraftList != null) { + statusDraftList.remove(statusDraft); + if (statusDraftList.size() == 0) { + scheduledActions.onAllDeleted(); + } + notifyItemRemoved(position); + } + } catch (DBException e) { + e.printStackTrace(); + } + } else if (scheduledBoost != null) { + try { + new ScheduledBoost(context).removeScheduled(scheduledBoost); + WorkManager.getInstance(context).cancelWorkById(scheduledBoost.workerUuid); + if (scheduledBoosts != null) { + scheduledBoosts.remove(scheduledBoost); + if (scheduledBoosts.size() == 0) { + scheduledActions.onAllDeleted(); + } + notifyItemRemoved(position); + } + } catch (DBException e) { + e.printStackTrace(); + } + } + dialog.dismiss(); + }); + unfollowConfirm.show(); + }); + + } + + public long getItemId(int position) { + return position; + } + + @Override + public int getItemCount() { + if (scheduledStatuses != null) { + return scheduledStatuses.size(); + } else if (scheduledBoosts != null) { + return scheduledBoosts.size(); + } else if (statusDraftList != null) { + return statusDraftList.size(); + } + return 0; + } + + public interface ScheduledActions { + void onAllDeleted(); + } + + static class StatusScheduledHolder extends RecyclerView.ViewHolder { + DrawerStatusScheduledBinding binding; + + StatusScheduledHolder(DrawerStatusScheduledBinding itemView) { + super(itemView.getRoot()); + binding = itemView; + } + } + + +} \ No newline at end of file diff --git a/app/src/main/java/app/fedilab/android/ui/drawer/TagAdapter.java b/app/src/main/java/app/fedilab/android/ui/drawer/TagAdapter.java new file mode 100644 index 000000000..cda34da81 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/ui/drawer/TagAdapter.java @@ -0,0 +1,140 @@ +package app.fedilab.android.ui.drawer; +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.core.content.ContextCompat; +import androidx.recyclerview.widget.RecyclerView; + +import com.github.mikephil.charting.components.Description; +import com.github.mikephil.charting.components.YAxis; +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.data.LineData; +import com.github.mikephil.charting.data.LineDataSet; +import com.github.mikephil.charting.interfaces.datasets.ILineDataSet; + +import java.util.ArrayList; +import java.util.List; + +import app.fedilab.android.R; +import app.fedilab.android.activities.HashTagActivity; +import app.fedilab.android.client.mastodon.entities.History; +import app.fedilab.android.client.mastodon.entities.Tag; +import app.fedilab.android.databinding.DrawerTagBinding; +import app.fedilab.android.helper.Helper; + +public class TagAdapter extends RecyclerView.Adapter { + private final List tagList; + private Context context; + + public TagAdapter(List tagList) { + this.tagList = tagList; + } + + + public static void tagManagement(Context context, TagViewHolder tagViewHolder, Tag tag) { + tagViewHolder.binding.tagName.setText(String.format("#%s", tag.name)); + + List trendsEntry = new ArrayList<>(); + + List historyList = tag.history; + + int stat = 0; + + + for (History history : historyList) { + trendsEntry.add(0, new Entry(Float.parseFloat(history.day), Float.parseFloat(history.uses))); + stat += Integer.parseInt(history.accounts); + } + tagViewHolder.binding.tagStats.setText(context.getString(R.string.talking_about, stat)); + LineDataSet dataTrending = new LineDataSet(trendsEntry, context.getString(R.string.trending)); + dataTrending.setColor(ContextCompat.getColor(context, R.color.cyanea_accent_reference)); + dataTrending.setValueTextColor(ContextCompat.getColor(context, R.color.cyanea_accent_reference)); + dataTrending.setFillColor(ContextCompat.getColor(context, R.color.cyanea_accent_reference)); + dataTrending.setDrawValues(false); + dataTrending.setDrawFilled(true); + dataTrending.setDrawCircles(false); + dataTrending.setDrawCircleHole(false); + tagViewHolder.binding.chart.getAxis(YAxis.AxisDependency.LEFT).setEnabled(false); + tagViewHolder.binding.chart.getAxis(YAxis.AxisDependency.RIGHT).setEnabled(false); + tagViewHolder.binding.chart.getXAxis().setEnabled(false); + tagViewHolder.binding.chart.getLegend().setEnabled(false); + tagViewHolder.binding.chart.setTouchEnabled(false); + dataTrending.setMode(LineDataSet.Mode.CUBIC_BEZIER); + Description description = tagViewHolder.binding.chart.getDescription(); + description.setEnabled(false); + List dataSets = new ArrayList<>(); + + + dataSets.add(dataTrending); + + LineData data = new LineData(dataSets); + tagViewHolder.binding.chart.setData(data); + tagViewHolder.binding.chart.invalidate(); + + + tagViewHolder.binding.getRoot().setOnClickListener(v1 -> { + Intent intent = new Intent(context, HashTagActivity.class); + Bundle b = new Bundle(); + b.putString(Helper.ARG_SEARCH_KEYWORD, tag.name.trim()); + intent.putExtras(b); + context.startActivity(intent); + }); + } + + public int getCount() { + return tagList.size(); + } + + public Tag getItem(int position) { + return tagList.get(position); + } + + @NonNull + @Override + public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + context = parent.getContext(); + DrawerTagBinding itemBinding = DrawerTagBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new TagViewHolder(itemBinding); + } + + @Override + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) { + Tag tag = tagList.get(position); + TagViewHolder holder = (TagViewHolder) viewHolder; + tagManagement(context, holder, tag); + } + + @Override + public int getItemCount() { + return tagList.size(); + } + + + public static class TagViewHolder extends RecyclerView.ViewHolder { + DrawerTagBinding binding; + + TagViewHolder(DrawerTagBinding itemView) { + super(itemView.getRoot()); + binding = itemView; + } + } +} diff --git a/app/src/main/java/app/fedilab/android/ui/drawer/TagsSearchAdapter.java b/app/src/main/java/app/fedilab/android/ui/drawer/TagsSearchAdapter.java new file mode 100644 index 000000000..57851c19e --- /dev/null +++ b/app/src/main/java/app/fedilab/android/ui/drawer/TagsSearchAdapter.java @@ -0,0 +1,134 @@ +package app.fedilab.android.ui.drawer; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.Filter; +import android.widget.Filterable; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.ArrayList; +import java.util.List; + +import app.fedilab.android.client.mastodon.entities.Tag; +import app.fedilab.android.databinding.DrawerTagSearchBinding; + +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +public class TagsSearchAdapter extends ArrayAdapter implements Filterable { + + private final List tags; + private final List tempTags; + private final List suggestions; + + private final Filter searchFilter = new Filter() { + @Override + public CharSequence convertResultToString(Object resultValue) { + Tag tag = (Tag) resultValue; + return "#" + tag.name; + } + + @Override + protected FilterResults performFiltering(CharSequence constraint) { + if (constraint != null) { + suggestions.clear(); + suggestions.addAll(tempTags); + FilterResults filterResults = new FilterResults(); + filterResults.values = suggestions; + filterResults.count = suggestions.size(); + return filterResults; + } else { + return new FilterResults(); + } + } + + @Override + protected void publishResults(CharSequence constraint, FilterResults results) { + ArrayList c = (ArrayList) results.values; + if (results.count > 0) { + clear(); + addAll(c); + notifyDataSetChanged(); + } else { + clear(); + notifyDataSetChanged(); + } + } + }; + + public TagsSearchAdapter(Context context, List tags) { + super(context, android.R.layout.simple_list_item_1, tags); + this.tags = tags; + this.tempTags = new ArrayList<>(tags); + this.suggestions = new ArrayList<>(tags); + } + + @Override + public int getCount() { + return tags.size(); + } + + @Override + public Tag getItem(int position) { + return tags.get(position); + } + + @Override + public long getItemId(int position) { + return position; + } + + @NonNull + @Override + public View getView(final int position, View convertView, @NonNull ViewGroup parent) { + + final Tag tag = tags.get(position); + TagSearchViewHolder holder; + if (convertView == null) { + DrawerTagSearchBinding drawerTagSearchBinding = DrawerTagSearchBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + holder = new TagSearchViewHolder(drawerTagSearchBinding); + holder.view = drawerTagSearchBinding.getRoot(); + holder.view.setTag(holder); + } else { + holder = (TagSearchViewHolder) convertView.getTag(); + } + holder.binding.tagName.setText(String.format("#%s", tag.name)); + + return holder.view; + } + + @NonNull + @Override + public Filter getFilter() { + return searchFilter; + } + + public static class TagSearchViewHolder extends RecyclerView.ViewHolder { + DrawerTagSearchBinding binding; + private View view; + + TagSearchViewHolder(DrawerTagSearchBinding itemView) { + super(itemView.getRoot()); + this.view = itemView.getRoot(); + binding = itemView; + } + } + +} diff --git a/app/src/main/java/app/fedilab/android/ui/drawer/TopMenuAdapter.java b/app/src/main/java/app/fedilab/android/ui/drawer/TopMenuAdapter.java new file mode 100644 index 000000000..5ae6ba9c0 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/ui/drawer/TopMenuAdapter.java @@ -0,0 +1,117 @@ +package app.fedilab.android.ui.drawer; +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.core.content.res.ResourcesCompat; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.List; + +import app.fedilab.android.R; +import app.fedilab.android.client.entities.Timeline; +import app.fedilab.android.client.entities.app.PinnedTimeline; +import app.fedilab.android.databinding.DrawerTopMenuItemBinding; + + +public class TopMenuAdapter extends RecyclerView.Adapter { + private final List pinnedTimelines; + public TopMenuClicked itemListener; + private Context _mContext; + + public TopMenuAdapter(List pinnedTimelines) { + this.pinnedTimelines = pinnedTimelines; + } + + public int getCount() { + return pinnedTimelines.size(); + } + + public PinnedTimeline getItem(int position) { + return pinnedTimelines.get(position); + } + + @NonNull + @Override + public TopMenuHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + _mContext = parent.getContext(); + DrawerTopMenuItemBinding itemBinding = DrawerTopMenuItemBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new TopMenuHolder(itemBinding); + } + + @Override + public void onBindViewHolder(@NonNull TopMenuHolder holder, int position) { + + PinnedTimeline pinnedTimeline = pinnedTimelines.get(position); + if (pinnedTimeline.displayed) { + String name = ""; + if (pinnedTimeline.type == Timeline.TimeLineEnum.LIST) { + name = pinnedTimeline.mastodonList.title; + } else if (pinnedTimeline.type == Timeline.TimeLineEnum.TAG) { + name = pinnedTimeline.tagTimeline.name; + } else if (pinnedTimeline.type == Timeline.TimeLineEnum.REMOTE) { + name = pinnedTimeline.remoteInstance.host; + } + holder.binding.name.setText(name); + holder.binding.getRoot().setVisibility(View.VISIBLE); + } else { + holder.binding.getRoot().setVisibility(View.GONE); + } + holder.binding.getRoot().setOnClickListener(v -> itemListener.onClick(v, pinnedTimeline, position)); + holder.binding.getRoot().setOnLongClickListener(v -> { + itemListener.onLongClick(holder.binding.getRoot(), pinnedTimeline, position); + return true; + }); + //Manage item decoration below the text + if (pinnedTimeline.isSelected) { + holder.binding.underline.setVisibility(View.VISIBLE); + holder.binding.name.setTextColor(ResourcesCompat.getColor(_mContext.getResources(), R.color.cyanea_accent, _mContext.getTheme())); + } else { + holder.binding.underline.setVisibility(View.GONE); + int textColor = _mContext.getResources().getColor(android.R.color.primary_text_dark); + holder.binding.name.setTextColor(textColor); + } + } + + public long getItemId(int position) { + return position; + } + + @Override + public int getItemCount() { + return pinnedTimelines.size(); + } + + public interface TopMenuClicked { + void onClick(View v, PinnedTimeline pinnedTimeline, int position); + + void onLongClick(View v, PinnedTimeline pinnedTimeline, int position); + } + + static class TopMenuHolder extends RecyclerView.ViewHolder { + DrawerTopMenuItemBinding binding; + + TopMenuHolder(DrawerTopMenuItemBinding itemView) { + super(itemView.getRoot()); + binding = itemView; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/fedilab/android/ui/fragment/login/FragmentLoginJoin.java b/app/src/main/java/app/fedilab/android/ui/fragment/login/FragmentLoginJoin.java new file mode 100644 index 000000000..fe035f9dc --- /dev/null +++ b/app/src/main/java/app/fedilab/android/ui/fragment/login/FragmentLoginJoin.java @@ -0,0 +1,53 @@ +package app.fedilab.android.ui.fragment.login; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; + +import app.fedilab.android.databinding.FragmentLoginJoinBinding; +import app.fedilab.android.helper.Helper; + + +public class FragmentLoginJoin extends Fragment { + + + private FragmentLoginJoinBinding binding; + + public View onCreateView(@NonNull LayoutInflater inflater, + ViewGroup container, Bundle savedInstanceState) { + + binding = FragmentLoginJoinBinding.inflate(inflater, container, false); + View root = binding.getRoot(); + binding.joinMastodon.setOnClickListener(v -> { + Helper.addFragment( + getParentFragmentManager(), android.R.id.content, new FragmentLoginPickInstanceMastodon(), + null, null, FragmentLoginPickInstanceMastodon.class.getName()); + }); + + return root; + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + binding = null; + } +} \ No newline at end of file diff --git a/app/src/main/java/app/fedilab/android/ui/fragment/login/FragmentLoginMain.java b/app/src/main/java/app/fedilab/android/ui/fragment/login/FragmentLoginMain.java new file mode 100644 index 000000000..849e63138 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/ui/fragment/login/FragmentLoginMain.java @@ -0,0 +1,275 @@ +package app.fedilab.android.ui.fragment.login; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import static app.fedilab.android.BaseMainActivity.admin; +import static app.fedilab.android.BaseMainActivity.api; +import static app.fedilab.android.BaseMainActivity.client_id; +import static app.fedilab.android.BaseMainActivity.client_secret; +import static app.fedilab.android.BaseMainActivity.currentInstance; + +import android.content.Intent; +import android.content.SharedPreferences; +import android.net.Uri; +import android.os.Bundle; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.ContextThemeWrapper; +import android.view.LayoutInflater; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.appcompat.widget.PopupMenu; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; +import androidx.preference.PreferenceManager; + +import java.io.UnsupportedEncodingException; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLEncoder; + +import app.fedilab.android.BaseMainActivity; +import app.fedilab.android.R; +import app.fedilab.android.activities.WebviewConnectActivity; +import app.fedilab.android.client.entities.Account; +import app.fedilab.android.client.entities.InstanceSocial; +import app.fedilab.android.databinding.FragmentLoginMainBinding; +import app.fedilab.android.helper.Helper; +import app.fedilab.android.helper.MastodonHelper; +import app.fedilab.android.viewmodel.mastodon.AppsVM; +import app.fedilab.android.viewmodel.mastodon.InstanceSocialVM; +import app.fedilab.android.viewmodel.mastodon.NodeInfoVM; +import es.dmoral.toasty.Toasty; + +public class FragmentLoginMain extends Fragment { + + private static boolean client_id_for_webview = false; + private FragmentLoginMainBinding binding; + private boolean searchInstanceRunning = false; + private String oldSearch; + + + public View onCreateView(@NonNull LayoutInflater inflater, + ViewGroup container, Bundle savedInstanceState) { + + + binding = FragmentLoginMainBinding.inflate(inflater, container, false); + View root = binding.getRoot(); + + binding.menuIcon.setOnClickListener(this::showMenu); + binding.loginInstance.setOnItemClickListener((parent, view, position, id) -> oldSearch = parent.getItemAtPosition(position).toString().trim()); + binding.loginInstance.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + + } + + @Override + public void afterTextChanged(Editable s) { + + if (s.length() > 2 && !searchInstanceRunning) { + String query = s.toString().trim(); + if (query.startsWith("http://")) { + query = query.replace("http://", ""); + } + if (query.startsWith("https://")) { + query = query.replace("https://", ""); + } + if (oldSearch == null || !oldSearch.equals(s.toString().trim())) { + searchInstanceRunning = true; + InstanceSocialVM instanceSocialVM = new ViewModelProvider(FragmentLoginMain.this).get(InstanceSocialVM.class); + instanceSocialVM.getInstances(query).observe(requireActivity(), instanceSocialList -> { + binding.loginInstance.setAdapter(null); + String[] instances = new String[instanceSocialList.instances.size()]; + int j = 0; + for (InstanceSocial.Instance instance : instanceSocialList.instances) { + instances[j] = instance.name; + j++; + } + ArrayAdapter adapter = + new ArrayAdapter<>(requireActivity(), android.R.layout.simple_list_item_1, instances); + binding.loginInstance.setAdapter(adapter); + if (binding.loginInstance.hasFocus() && !requireActivity().isFinishing()) + binding.loginInstance.showDropDown(); + if (oldSearch != null && oldSearch.equals(binding.loginInstance.getText().toString())) { + binding.loginInstance.dismissDropDown(); + } + + oldSearch = s.toString().trim(); + searchInstanceRunning = false; + }); + } + } + } + }); + + binding.noAccountA.setOnClickListener(v -> Helper.addFragment( + getParentFragmentManager(), android.R.id.content, new FragmentLoginJoin(), + null, null, FragmentLoginJoin.class.getName())); + + binding.continueButton.setOnClickListener(v -> { + if (binding.loginInstance.getText() == null || binding.loginInstance.getText().toString().length() == 0) { + binding.loginInstanceLayout.setError(getString(R.string.toast_error_instance)); + binding.loginInstanceLayout.setErrorEnabled(true); + return; + } + currentInstance = binding.loginInstance.getText().toString().trim().toLowerCase(); + if (currentInstance.length() == 0) { + return; + } + binding.continueButton.setEnabled(false); + NodeInfoVM nodeInfoVM = new ViewModelProvider(requireActivity()).get(NodeInfoVM.class); + nodeInfoVM.getNodeInfo(binding.loginInstance.getText().toString()).observe(requireActivity(), nodeInfo -> { + binding.continueButton.setEnabled(true); + if (nodeInfo != null && nodeInfo.software != null) { + BaseMainActivity.software = nodeInfo.software.name.toUpperCase(); + if (nodeInfo.software.name.toUpperCase().trim().equals("PLEROMA") || nodeInfo.software.name.toUpperCase().trim().equals("MASTODON") || nodeInfo.software.name.toUpperCase().trim().equals("PIXELFED")) { + client_id_for_webview = true; + api = Account.API.MASTODON; + retrievesClientId(currentInstance); + } else { + client_id_for_webview = false; + if (nodeInfo.software.name.equals("PEERTUBE")) { + Toasty.error(requireActivity(), "Peertube is currently not supported", Toasty.LENGTH_LONG).show(); + } else if (nodeInfo.software.name.equals("GNU")) { + Toasty.error(requireActivity(), "GNU is currently not supported", Toasty.LENGTH_LONG).show(); + } else { //Fallback to Mastodon + client_id_for_webview = true; + api = Account.API.MASTODON; + retrievesClientId(currentInstance); + } + } + } else { //Fallback to Mastodon + client_id_for_webview = true; + api = Account.API.MASTODON; + retrievesClientId(currentInstance); + } + }); + }); + return root; + } + + private void showMenu(View v) { + PopupMenu popupMenu = new PopupMenu(new ContextThemeWrapper(requireActivity(), Helper.popupStyle()), binding.menuIcon); + MenuInflater menuInflater = popupMenu.getMenuInflater(); + menuInflater.inflate(R.menu.main_login, popupMenu.getMenu()); + popupMenu.setOnMenuItemClickListener(item -> { + int itemId = item.getItemId(); + if (itemId == R.id.action_about) { + /* Todo: Open about page */ + } else if (itemId == R.id.action_privacy) { + /* Todo: Open privacy page */ + } else if (itemId == R.id.action_proxy) { + /* Todo: Open proxy settings */ + } else if (itemId == R.id.action_custom_tabs) { + setMenuItemKeepOpen(item); + /* Todo: Toggle custom tabs */ + } else if (itemId == R.id.action_import_data) { + /* Todo: Import data */ + } else if (itemId == R.id.action_provider) { + setMenuItemKeepOpen(item); + /* Todo: Toggle security provider */ + } + return false; + }); + popupMenu.show(); + } + + private void setMenuItemKeepOpen(MenuItem item) { + item.setShowAsAction(MenuItem.SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW); + item.setActionView(new View(requireContext())); + item.setOnActionExpandListener(new MenuItem.OnActionExpandListener() { + @Override + public boolean onMenuItemActionExpand(MenuItem item) { + return false; + } + + @Override + public boolean onMenuItemActionCollapse(MenuItem item) { + return false; + } + }); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + binding = null; + } + + private void retrievesClientId(String instance) { + if (client_id_for_webview) { + if (!instance.startsWith("http://") && !instance.startsWith("https://")) { + instance = "https://" + instance; + } + String host = instance; + try { + URL url = new URL(instance); + host = url.getHost(); + } catch (MalformedURLException e) { + e.printStackTrace(); + } + + try { + currentInstance = URLEncoder.encode(host, "utf-8"); + } catch (UnsupportedEncodingException e) { + Toasty.error(requireActivity(), getString(R.string.client_error), Toast.LENGTH_LONG).show(); + } + if (api == Account.API.MASTODON) { + String scopes = Helper.OAUTH_SCOPES; + if (admin) { + scopes = Helper.OAUTH_SCOPES_ADMIN; + } + AppsVM appsVM = new ViewModelProvider(requireActivity()).get(AppsVM.class); + appsVM.createApp(currentInstance, getString(R.string.app_name), + client_id_for_webview ? Helper.REDIRECT_CONTENT_WEB : Helper.REDIRECT_CONTENT, + scopes, + Helper.WEBSITE_VALUE + ).observe(requireActivity(), app -> { + client_id = app.client_id; + client_secret = app.client_secret; + String redirectUrl = MastodonHelper.authorizeURL(currentInstance, client_id, admin); + SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(requireActivity()); + boolean embedded_browser = sharedpreferences.getBoolean(getString(R.string.SET_EMBEDDED_BROWSER), true); + if (embedded_browser) { + Intent i = new Intent(requireActivity(), WebviewConnectActivity.class); + i.putExtra("login_url", redirectUrl); + startActivity(i); + } else { + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.setData(Uri.parse(redirectUrl)); + try { + startActivity(intent); + } catch (Exception e) { + Toasty.error(requireActivity(), getString(R.string.toast_error), Toast.LENGTH_LONG).show(); + } + + } + }); + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/fedilab/android/ui/fragment/login/FragmentLoginPickInstanceMastodon.java b/app/src/main/java/app/fedilab/android/ui/fragment/login/FragmentLoginPickInstanceMastodon.java new file mode 100644 index 000000000..0a3caa0be --- /dev/null +++ b/app/src/main/java/app/fedilab/android/ui/fragment/login/FragmentLoginPickInstanceMastodon.java @@ -0,0 +1,139 @@ +package app.fedilab.android.ui.fragment.login; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; + +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; +import androidx.recyclerview.widget.LinearLayoutManager; + +import java.util.List; + +import app.fedilab.android.R; +import app.fedilab.android.client.mastodon.entities.JoinMastodonInstance; +import app.fedilab.android.databinding.FragmentLoginPickInstanceMastodonBinding; +import app.fedilab.android.helper.Helper; +import app.fedilab.android.ui.drawer.InstanceRegAdapter; +import app.fedilab.android.viewmodel.mastodon.JoinInstancesVM; + +public class FragmentLoginPickInstanceMastodon extends Fragment implements InstanceRegAdapter.RecyclerViewClickListener { + + + private List joinMastodonInstanceList; + + private FragmentLoginPickInstanceMastodonBinding binding; + private FragmentLoginPickInstanceMastodon currentFragment; + + public View onCreateView(@NonNull LayoutInflater inflater, + ViewGroup container, Bundle savedInstanceState) { + + + binding = FragmentLoginPickInstanceMastodonBinding.inflate(inflater, container, false); + View root = binding.getRoot(); + + + String[] categoriesA = { + getString(R.string.category_general), + getString(R.string.category_regional), + getString(R.string.category_art), + getString(R.string.category_music), + getString(R.string.category_activism), + "LGBTQ+", + getString(R.string.category_games), + getString(R.string.category_tech), + getString(R.string.category_furry), + getString(R.string.category_food), + getString(R.string.category_custom), + + }; + String[] itemA = { + "general", + "regional", + "art", + "music", + "activism", + "lgbt", + "games", + "tech", + "furry", + "food", + "custom" + }; + ArrayAdapter adcategories = new ArrayAdapter<>(requireActivity(), + android.R.layout.simple_spinner_dropdown_item, categoriesA); + currentFragment = this; + binding.regCategory.setAdapter(adcategories); + binding.regCategory.setSelection(0); + //Manage privacies + binding.regCategory.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + if (itemA[position].compareTo("custom") != 0) { + JoinInstancesVM joinInstancesVM = new ViewModelProvider(requireActivity()).get(JoinInstancesVM.class); + joinInstancesVM.getInstances(itemA[position]).observe(requireActivity(), instances -> { + joinMastodonInstanceList = instances; + if (instances != null) { + InstanceRegAdapter instanceRegAdapter = new InstanceRegAdapter(instances); + instanceRegAdapter.itemListener = currentFragment; + LinearLayoutManager mLayoutManager = new LinearLayoutManager(requireActivity()); + binding.regCategoryView.setLayoutManager(mLayoutManager); + binding.regCategoryView.setNestedScrollingEnabled(false); + binding.regCategoryView.setAdapter(instanceRegAdapter); + } + }); + } else { + binding.regCategory.setSelection(0); + Helper.addFragment( + getParentFragmentManager(), android.R.id.content, new FragmentLoginRegisterMastodon(), + null, null, FragmentLoginRegisterMastodon.class.getName()); + } + } + + @Override + public void onNothingSelected(AdapterView parent) { + + } + }); + + return root; + } + + + @Override + public void recyclerViewListClicked(View v, int position) { + if (joinMastodonInstanceList != null) { + JoinMastodonInstance clickedInstance = joinMastodonInstanceList.get(position); + Bundle args = new Bundle(); + args.putString("instance", clickedInstance.domain); + Helper.addFragment( + getParentFragmentManager(), android.R.id.content, new FragmentLoginRegisterMastodon(), + args, null, FragmentLoginRegisterMastodon.class.getName()); + } + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + binding = null; + } +} \ No newline at end of file diff --git a/app/src/main/java/app/fedilab/android/ui/fragment/login/FragmentLoginRegisterMastodon.java b/app/src/main/java/app/fedilab/android/ui/fragment/login/FragmentLoginRegisterMastodon.java new file mode 100644 index 000000000..628f38e22 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/ui/fragment/login/FragmentLoginRegisterMastodon.java @@ -0,0 +1,198 @@ +package app.fedilab.android.ui.fragment.login; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + + +import android.os.Bundle; +import android.text.Html; +import android.text.method.LinkMovementMethod; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; + +import java.util.Locale; + +import app.fedilab.android.R; +import app.fedilab.android.databinding.FragmentLoginRegisterMastodonBinding; +import app.fedilab.android.helper.Helper; +import app.fedilab.android.viewmodel.mastodon.AccountsVM; +import app.fedilab.android.viewmodel.mastodon.AppsVM; +import app.fedilab.android.viewmodel.mastodon.NodeInfoVM; +import app.fedilab.android.viewmodel.mastodon.OauthVM; + +public class FragmentLoginRegisterMastodon extends Fragment { + + + private FragmentLoginRegisterMastodonBinding binding; + private NodeInfoVM nodeInfoVM; + + public View onCreateView(@NonNull LayoutInflater inflater, + ViewGroup container, Bundle savedInstanceState) { + + Bundle args = getArguments(); + String instance = null; + if (args != null) { + instance = args.getString("instance", null); + } + + binding = FragmentLoginRegisterMastodonBinding.inflate(inflater, container, false); + View root = binding.getRoot(); + + nodeInfoVM = new ViewModelProvider(requireActivity()).get(NodeInfoVM.class); + if (instance != null) { + binding.loginInstance.setText(instance.trim()); + binding.loginInstance.setEnabled(false); + String tos = getString(R.string.tos); + String serverrules = getString(R.string.server_rules); + String content_agreement = getString(R.string.agreement_check, + "" + serverrules + "", + "" + tos + "" + ); + binding.agreementText.setMovementMethod(LinkMovementMethod.getInstance()); + binding.agreementText.setText(Html.fromHtml(content_agreement)); + } else { + binding.loginInstance.setOnFocusChangeListener((v, hasFocus) -> { + if (!hasFocus) { + nodeInfoVM.getNodeInfo(binding.loginInstance.getText().toString()).observe(requireActivity(), nodeInfo -> { + if (nodeInfo != null && (nodeInfo.software.name.trim().toLowerCase().compareTo("mastodon") == 0 || nodeInfo.software.name.trim().toLowerCase().compareTo("pleroma") == 0)) { + String tos = getString(R.string.tos); + String serverrules = getString(R.string.server_rules); + String content_agreement = getString(R.string.agreement_check, + "" + serverrules + "", + "" + tos + "" + ); + binding.agreementText.setMovementMethod(LinkMovementMethod.getInstance()); + binding.agreementText.setText(Html.fromHtml(content_agreement)); + } else { + binding.loginInstanceLayout.setError(getString(R.string.instance_not_valid)); + } + }); + } + }); + } + + binding.signup.setOnClickListener(v -> { + boolean error = false; + binding.loginUsernameLayout.setError(null); + binding.loginEmailLayout.setError(null); + binding.loginInstanceLayout.setError(null); + binding.loginPasswordLayout.setError(null); + binding.loginPasswordConfirmLayout.setError(null); + + if (binding.loginUsername.getText().toString().trim().length() == 0) { + binding.loginUsernameLayout.setError(getString(R.string.cannot_be_empty)); + error = true; + } + if (binding.loginEmail.getText().toString().trim().length() == 0) { + binding.loginEmailLayout.setError(getString(R.string.cannot_be_empty)); + error = true; + } + if (binding.loginInstance.getText().toString().trim().length() == 0) { + binding.loginInstanceLayout.setError(getString(R.string.cannot_be_empty)); + error = true; + } else { + + nodeInfoVM.getNodeInfo(binding.loginInstance.getText().toString()).observe(requireActivity(), nodeInfo -> { + if (nodeInfo == null || (nodeInfo.software.name.trim().toLowerCase().compareTo("mastodon") != 0 && nodeInfo.software.name.trim().toLowerCase().compareTo("pleroma") != 0)) { + binding.loginInstanceLayout.setError(getString(R.string.instance_not_valid)); + } + }); + } + if (binding.loginPassword.getText().toString().trim().length() == 0) { + binding.loginPasswordLayout.setError(getString(R.string.cannot_be_empty)); + error = true; + } + + if (!binding.loginPassword.getText().toString().trim().equals(binding.loginPasswordConfirm.getText().toString().trim())) { + binding.loginPasswordConfirmLayout.setError(getString(R.string.password_error)); + error = true; + } + if (binding.loginPassword.getText().toString().trim().length() < 8) { + binding.loginPasswordLayout.setError(getString(R.string.password_too_short)); + error = true; + } + if (!android.util.Patterns.EMAIL_ADDRESS.matcher(binding.loginEmail.getText().toString().trim()).matches()) { + binding.loginEmailLayout.setError(getString(R.string.email_error)); + error = true; + } + if (binding.loginUsername.getText() == null || binding.loginUsername.getText().toString().trim().length() == 0) { + binding.loginUsernameLayout.setError(getString(R.string.cannot_be_empty)); + error = true; + } + if (binding.loginUsername.getText().toString().matches("[a-zA-Z0-9_]")) { + binding.loginUsernameLayout.setError(getString(R.string.username_error)); + error = true; + } + + if (error) { + return; + } + String registerInstance = binding.loginInstance.getText().toString().trim(); + AppsVM appsVM = new ViewModelProvider(requireActivity()).get(AppsVM.class); + appsVM.createApp(registerInstance, getString(R.string.app_name), + Helper.REDIRECT_CONTENT_WEB, + Helper.OAUTH_SCOPES, + Helper.WEBSITE_VALUE + ).observe(requireActivity(), app -> { + OauthVM oauthVM = new ViewModelProvider(requireActivity()).get(OauthVM.class); + oauthVM.createToken(registerInstance, "client_credentials", app.client_id, app.client_secret, null, Helper.APP_OAUTH_SCOPES, null) + .observe(requireActivity(), tokenObj -> { + AccountsVM accountsVM = new ViewModelProvider(requireActivity()).get(AccountsVM.class); + accountsVM.registerAccount( + registerInstance, + tokenObj.token_type + " " + tokenObj.access_token, + binding.loginUsername.getText().toString().trim(), + binding.loginEmail.getText().toString().trim(), + binding.loginPassword.getText().toString().trim(), + binding.agreement.isChecked(), + Locale.getDefault().getLanguage(), null + ).observe(requireActivity(), token -> { + if (token != null) { + AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(requireActivity(), Helper.dialogStyle()); + dialogBuilder.setCancelable(false); + dialogBuilder.setPositiveButton(R.string.validate, (dialog, which) -> { + dialog.dismiss(); + requireActivity().onBackPressed(); + requireActivity().onBackPressed(); + }); + AlertDialog alertDialog = dialogBuilder.create(); + alertDialog.setTitle(getString(R.string.account_created)); + alertDialog.setMessage(getString(R.string.account_created_message, registerInstance)); + alertDialog.show(); + //Revoke the current token as we will not use it immediately. + oauthVM.revokeToken(registerInstance, tokenObj.token_type + " " + tokenObj.access_token, app.client_id, app.client_secret); + } + + }); + }); + + + }); + + }); + return root; + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + binding = null; + } +} \ No newline at end of file diff --git a/app/src/main/java/app/fedilab/android/ui/fragment/media/FragmentMedia.java b/app/src/main/java/app/fedilab/android/ui/fragment/media/FragmentMedia.java new file mode 100644 index 000000000..e2a295d8f --- /dev/null +++ b/app/src/main/java/app/fedilab/android/ui/fragment/media/FragmentMedia.java @@ -0,0 +1,330 @@ +package app.fedilab.android.ui.fragment.media; +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import android.content.SharedPreferences; +import android.graphics.Bitmap; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.view.WindowManager; +import android.widget.ImageView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.app.ActivityCompat; +import androidx.fragment.app.Fragment; +import androidx.preference.PreferenceManager; + +import com.arges.sepan.argmusicplayer.Models.ArgAudio; +import com.bumptech.glide.Glide; +import com.bumptech.glide.request.target.CustomTarget; +import com.bumptech.glide.request.transition.Transition; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.source.ProgressiveMediaSource; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; +import com.google.android.exoplayer2.util.Util; + +import java.util.Timer; + +import app.fedilab.android.R; +import app.fedilab.android.activities.MediaActivity; +import app.fedilab.android.client.mastodon.entities.Attachment; +import app.fedilab.android.databinding.FragmentSlideMediaBinding; +import app.fedilab.android.helper.CacheDataSourceFactory; +import app.fedilab.android.helper.Helper; +import app.fedilab.android.webview.CustomWebview; +import app.fedilab.android.webview.FedilabWebChromeClient; +import app.fedilab.android.webview.FedilabWebViewClient; + + +public class FragmentMedia extends Fragment { + + + private SimpleExoPlayer player; + private Timer timer; + private String url; + private boolean canSwipe; + private Attachment attachment; + private boolean swipeEnabled; + private CustomWebview webview_video; + private FragmentSlideMediaBinding binding; + private ArgAudio audio; + + public FragmentMedia() { + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + + binding = FragmentSlideMediaBinding.inflate(inflater, container, false); + Bundle bundle = this.getArguments(); + if (bundle != null) { + attachment = (Attachment) bundle.getSerializable(Helper.ARG_MEDIA_ATTACHMENT); + } + return binding.getRoot(); + } + + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + swipeEnabled = true; + SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(requireActivity()); + + url = attachment.url; + binding.mediaPicture.setOnMatrixChangeListener(rect -> { + canSwipe = (binding.mediaPicture.getScale() == 1); + + if (!canSwipe) { + if (!((MediaActivity) requireActivity()).getFullScreen()) { + ((MediaActivity) requireActivity()).setFullscreen(true); + } + enableSliding(false); + } else { + enableSliding(true); + } + }); + String type = attachment.type; + String preview_url = attachment.preview_url; + if (type.equalsIgnoreCase("unknown")) { + preview_url = attachment.remote_url; + if (preview_url.toLowerCase().endsWith(".png") || preview_url.toLowerCase().endsWith(".jpg") || preview_url.toLowerCase().endsWith(".jpeg") || preview_url.toLowerCase().endsWith(".gif")) { + type = "image"; + } else if (preview_url.toLowerCase().endsWith(".mp4") || preview_url.toLowerCase().endsWith(".mp3")) { + type = "video"; + } + url = attachment.remote_url; + attachment.type = type; + } + + binding.mediaPicture.setVisibility(View.VISIBLE); + binding.mediaPicture.setTransitionName(attachment.url); + if (Helper.isValidContextForGlide(requireActivity())) { + Glide.with(requireActivity()) + .asBitmap() + .dontTransform() + .load(preview_url).into( + new CustomTarget() { + @Override + public void onResourceReady(@NonNull final Bitmap resource, Transition transition) { + binding.mediaPicture.setImageBitmap(resource); + scheduleStartPostponedTransition(binding.mediaPicture); + if (attachment.type.equalsIgnoreCase("image") && !attachment.url.toLowerCase().endsWith(".gif")) { + final Handler handler = new Handler(); + handler.postDelayed(() -> { + if (binding == null) { + return; + } + binding.pbarInf.setScaleY(1f); + binding.mediaPicture.setVisibility(View.VISIBLE); + binding.pbarInf.setIndeterminate(true); + binding.loader.setVisibility(View.VISIBLE); + if (Helper.isValidContextForGlide(requireActivity())) { + Glide.with(requireActivity()) + .asBitmap() + .dontTransform() + .load(url).into( + new CustomTarget() { + @Override + public void onResourceReady(@NonNull final Bitmap resource, Transition transition) { + binding.loader.setVisibility(View.GONE); + if (binding.mediaPicture.getScale() < 1.1) { + binding.mediaPicture.setImageBitmap(resource); + } else { + binding.messageReady.setVisibility(View.VISIBLE); + } + binding.messageReady.setOnClickListener(view -> { + binding.mediaPicture.setImageBitmap(resource); + binding.messageReady.setVisibility(View.GONE); + }); + } + + @Override + public void onLoadCleared(@Nullable Drawable placeholder) { + + } + } + ); + } + }, 1000); + + + } else if (attachment.type.equalsIgnoreCase("image") && attachment.url.toLowerCase().endsWith(".gif")) { + binding.loader.setVisibility(View.GONE); + if (Helper.isValidContextForGlide(requireActivity())) { + Glide.with(requireActivity()) + .load(url).into(binding.mediaPicture); + } + scheduleStartPostponedTransition(binding.mediaPicture); + } + } + + @Override + public void onLoadFailed(@Nullable Drawable errorDrawable) { + scheduleStartPostponedTransition(binding.mediaPicture); + } + + @Override + public void onLoadCleared(@Nullable Drawable placeholder) { + + } + } + ); + } + switch (type.toLowerCase()) { + case "video": + case "audio": + case "gifv": + binding.pbarInf.setIndeterminate(false); + binding.pbarInf.setScaleY(3f); + binding.mediaVideo.setVisibility(View.VISIBLE); + Uri uri = Uri.parse(url); + + String userAgent = sharedpreferences.getString(getString(R.string.SET_CUSTOM_USER_AGENT), Helper.USER_AGENT); + int video_cache = sharedpreferences.getInt(getString(R.string.SET_VIDEO_CACHE), Helper.DEFAULT_VIDEO_CACHE_MB); + ProgressiveMediaSource videoSource; + if (video_cache == 0) { + DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(requireActivity(), + Util.getUserAgent(requireActivity(), userAgent), null); + videoSource = new ProgressiveMediaSource.Factory(dataSourceFactory) + .createMediaSource(uri); + } else { + CacheDataSourceFactory cacheDataSourceFactory = new CacheDataSourceFactory(requireActivity()); + videoSource = new ProgressiveMediaSource.Factory(cacheDataSourceFactory) + .createMediaSource(uri); + } + player = new SimpleExoPlayer.Builder(requireActivity()).build(); + if (type.equalsIgnoreCase("gifv")) + player.setRepeatMode(Player.REPEAT_MODE_ONE); + binding.mediaVideo.setPlayer(player); + binding.loader.setVisibility(View.GONE); + binding.mediaPicture.setVisibility(View.GONE); + player.prepare(videoSource); + player.setPlayWhenReady(true); + break; + case "web": + binding.loader.setVisibility(View.GONE); + binding.mediaPicture.setVisibility(View.GONE); + webview_video = Helper.initializeWebview(requireActivity(), R.id.webview_video, binding.getRoot()); + webview_video.setVisibility(View.VISIBLE); + FedilabWebChromeClient fedilabWebChromeClient = new FedilabWebChromeClient(requireActivity(), webview_video, binding.mainMediaFrame, binding.videoLayout); + fedilabWebChromeClient.setOnToggledFullscreen(fullscreen -> { + if (fullscreen) { + binding.videoLayout.setVisibility(View.VISIBLE); + WindowManager.LayoutParams attrs = (requireActivity()).getWindow().getAttributes(); + attrs.flags |= WindowManager.LayoutParams.FLAG_FULLSCREEN; + attrs.flags |= WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON; + (requireActivity()).getWindow().setAttributes(attrs); + (requireActivity()).getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE); + } else { + WindowManager.LayoutParams attrs = (requireActivity()).getWindow().getAttributes(); + attrs.flags &= ~WindowManager.LayoutParams.FLAG_FULLSCREEN; + attrs.flags &= ~WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON; + (requireActivity()).getWindow().setAttributes(attrs); + (requireActivity()).getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE); + binding.videoLayout.setVisibility(View.GONE); + } + }); + webview_video.getSettings().setAllowFileAccess(true); + webview_video.setWebChromeClient(fedilabWebChromeClient); + webview_video.getSettings().setDomStorageEnabled(true); + webview_video.getSettings().setAppCacheEnabled(true); + String user_agent = sharedpreferences.getString(getString(R.string.SET_CUSTOM_USER_AGENT), Helper.USER_AGENT); + webview_video.getSettings().setUserAgentString(user_agent); + webview_video.getSettings().setMediaPlaybackRequiresUserGesture(false); + webview_video.setWebViewClient(new FedilabWebViewClient(requireActivity())); + webview_video.loadUrl(attachment.url); + break; + } + } + + @Override + public void onCreate(Bundle saveInstance) { + super.onCreate(saveInstance); + } + + + @Override + public void onPause() { + super.onPause(); + if (player != null) { + player.setPlayWhenReady(false); + } + if (webview_video != null) { + webview_video.onPause(); + } + + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + try { + if (player != null) { + player.release(); + } + } catch (Exception ignored) { + } + if (webview_video != null) { + webview_video.destroy(); + } + if (timer != null) { + timer.cancel(); + timer = null; + } + binding = null; + } + + @Override + public void onResume() { + super.onResume(); + if (player != null) { + player.setPlayWhenReady(true); + } + + if (webview_video != null) { + webview_video.onResume(); + } + + } + + private void scheduleStartPostponedTransition(final ImageView imageView) { + imageView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { + @Override + public boolean onPreDraw() { + imageView.getViewTreeObserver().removeOnPreDrawListener(this); + ActivityCompat.startPostponedEnterTransition(requireActivity()); + return true; + } + }); + } + + private void enableSliding(boolean enable) { + if (enable && !swipeEnabled) { + swipeEnabled = true; + } else if (!enable && swipeEnabled) { + swipeEnabled = false; + } + } + +} diff --git a/app/src/main/java/app/fedilab/android/ui/fragment/settings/FragmentAdministrationSettings.java b/app/src/main/java/app/fedilab/android/ui/fragment/settings/FragmentAdministrationSettings.java new file mode 100644 index 000000000..1a51d9492 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/ui/fragment/settings/FragmentAdministrationSettings.java @@ -0,0 +1,63 @@ +package app.fedilab.android.ui.fragment.settings; +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import android.content.SharedPreferences; +import android.os.Bundle; + +import androidx.preference.PreferenceFragmentCompat; +import androidx.preference.PreferenceManager; + +import app.fedilab.android.R; + +public class FragmentAdministrationSettings extends PreferenceFragmentCompat implements SharedPreferences.OnSharedPreferenceChangeListener { + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + addPreferencesFromResource(R.xml.pref_administration); + createPref(); + } + + private void createPref() { + + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + if (getActivity() != null) { + SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(requireActivity()); + SharedPreferences.Editor editor = sharedpreferences.edit(); + + editor.apply(); + } + } + + @Override + public void onResume() { + super.onResume(); + + getPreferenceScreen().getSharedPreferences() + .registerOnSharedPreferenceChangeListener(this); + } + + @Override + public void onPause() { + super.onPause(); + getPreferenceScreen().getSharedPreferences() + .unregisterOnSharedPreferenceChangeListener(this); + } + + +} diff --git a/app/src/main/java/app/fedilab/android/ui/fragment/settings/FragmentComposeSettings.java b/app/src/main/java/app/fedilab/android/ui/fragment/settings/FragmentComposeSettings.java new file mode 100644 index 000000000..255dd8acc --- /dev/null +++ b/app/src/main/java/app/fedilab/android/ui/fragment/settings/FragmentComposeSettings.java @@ -0,0 +1,63 @@ +package app.fedilab.android.ui.fragment.settings; +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import android.content.SharedPreferences; +import android.os.Bundle; + +import androidx.preference.PreferenceFragmentCompat; +import androidx.preference.PreferenceManager; + +import app.fedilab.android.R; + +public class FragmentComposeSettings extends PreferenceFragmentCompat implements SharedPreferences.OnSharedPreferenceChangeListener { + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + addPreferencesFromResource(R.xml.pref_compose); + createPref(); + } + + private void createPref() { + + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + if (getActivity() != null) { + SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(requireActivity()); + SharedPreferences.Editor editor = sharedpreferences.edit(); + + editor.apply(); + } + } + + @Override + public void onResume() { + super.onResume(); + + getPreferenceScreen().getSharedPreferences() + .registerOnSharedPreferenceChangeListener(this); + } + + @Override + public void onPause() { + super.onPause(); + getPreferenceScreen().getSharedPreferences() + .unregisterOnSharedPreferenceChangeListener(this); + } + + +} diff --git a/app/src/main/java/app/fedilab/android/ui/fragment/settings/FragmentInterfaceSettings.java b/app/src/main/java/app/fedilab/android/ui/fragment/settings/FragmentInterfaceSettings.java new file mode 100644 index 000000000..4fe8e831e --- /dev/null +++ b/app/src/main/java/app/fedilab/android/ui/fragment/settings/FragmentInterfaceSettings.java @@ -0,0 +1,63 @@ +package app.fedilab.android.ui.fragment.settings; +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import android.content.SharedPreferences; +import android.os.Bundle; + +import androidx.preference.PreferenceFragmentCompat; +import androidx.preference.PreferenceManager; + +import app.fedilab.android.R; + +public class FragmentInterfaceSettings extends PreferenceFragmentCompat implements SharedPreferences.OnSharedPreferenceChangeListener { + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + addPreferencesFromResource(R.xml.pref_interface); + createPref(); + } + + private void createPref() { + + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + if (getActivity() != null) { + SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(requireActivity()); + SharedPreferences.Editor editor = sharedpreferences.edit(); + + editor.apply(); + } + } + + @Override + public void onResume() { + super.onResume(); + + getPreferenceScreen().getSharedPreferences() + .registerOnSharedPreferenceChangeListener(this); + } + + @Override + public void onPause() { + super.onPause(); + getPreferenceScreen().getSharedPreferences() + .unregisterOnSharedPreferenceChangeListener(this); + } + + +} diff --git a/app/src/main/java/app/fedilab/android/ui/fragment/settings/FragmentLanguageSettings.java b/app/src/main/java/app/fedilab/android/ui/fragment/settings/FragmentLanguageSettings.java new file mode 100644 index 000000000..a0120611c --- /dev/null +++ b/app/src/main/java/app/fedilab/android/ui/fragment/settings/FragmentLanguageSettings.java @@ -0,0 +1,66 @@ +package app.fedilab.android.ui.fragment.settings; +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import android.content.SharedPreferences; +import android.os.Bundle; + +import androidx.preference.ListPreference; +import androidx.preference.PreferenceFragmentCompat; + +import app.fedilab.android.R; +import app.fedilab.android.helper.Helper; +import app.fedilab.android.helper.ThemeHelper; + +public class FragmentLanguageSettings extends PreferenceFragmentCompat implements SharedPreferences.OnSharedPreferenceChangeListener { + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + addPreferencesFromResource(R.xml.pref_language); + createPref(); + } + + private void createPref() { + ListPreference SET_DEFAULT_LOCALE_NEW = findPreference(getString(R.string.SET_DEFAULT_LOCALE_NEW)); + if (SET_DEFAULT_LOCALE_NEW != null) { + SET_DEFAULT_LOCALE_NEW.getContext().setTheme(Helper.dialogStyle()); + } + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + if (key.compareToIgnoreCase(getString(R.string.SET_DEFAULT_LOCALE_NEW)) == 0) { + requireActivity().recreate(); + ThemeHelper.recreateMainActivity(requireActivity()); + } + } + + @Override + public void onResume() { + super.onResume(); + + getPreferenceScreen().getSharedPreferences() + .registerOnSharedPreferenceChangeListener(this); + } + + @Override + public void onPause() { + super.onPause(); + getPreferenceScreen().getSharedPreferences() + .unregisterOnSharedPreferenceChangeListener(this); + } + + +} diff --git a/app/src/main/java/app/fedilab/android/ui/fragment/settings/FragmentNotificationsSettings.java b/app/src/main/java/app/fedilab/android/ui/fragment/settings/FragmentNotificationsSettings.java new file mode 100644 index 000000000..ecd84ab23 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/ui/fragment/settings/FragmentNotificationsSettings.java @@ -0,0 +1,204 @@ +package app.fedilab.android.ui.fragment.settings; +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.content.Intent; +import android.content.SharedPreferences; +import android.os.Build; +import android.os.Bundle; +import android.provider.Settings; + +import androidx.annotation.NonNull; +import androidx.fragment.app.DialogFragment; +import androidx.preference.ListPreference; +import androidx.preference.Preference; +import androidx.preference.PreferenceCategory; +import androidx.preference.PreferenceFragmentCompat; +import androidx.preference.PreferenceManager; +import androidx.preference.PreferenceScreen; + +import app.fedilab.android.R; +import app.fedilab.android.helper.Helper; +import app.fedilab.android.helper.PushHelper; +import app.fedilab.android.helper.settings.TimePreference; +import app.fedilab.android.helper.settings.TimePreferenceDialogFragment; +import es.dmoral.toasty.Toasty; + + +public class FragmentNotificationsSettings extends PreferenceFragmentCompat implements SharedPreferences.OnSharedPreferenceChangeListener { + + private static final String DIALOG_FRAGMENT_TAG = "TimePreference"; + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + addPreferencesFromResource(R.xml.pref_notifications); + createPref(); + } + + @Override + public void onDisplayPreferenceDialog(@NonNull Preference preference) { + if (getParentFragmentManager().findFragmentByTag(DIALOG_FRAGMENT_TAG) != null) { + return; + } + + if (preference instanceof TimePreference) { + final DialogFragment f = TimePreferenceDialogFragment.newInstance(preference.getKey()); + f.setTargetFragment(this, 0); + f.show(getParentFragmentManager(), DIALOG_FRAGMENT_TAG); + } else { + super.onDisplayPreferenceDialog(preference); + } + } + + private void createPref() { + + getPreferenceScreen().removeAll(); + addPreferencesFromResource(R.xml.pref_notifications); + PreferenceScreen preferenceScreen = getPreferenceScreen(); + if (preferenceScreen == null) { + Toasty.error(requireActivity(), getString(R.string.toast_error), Toasty.LENGTH_SHORT).show(); + return; + } + + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(requireActivity()); + ListPreference SET_NOTIFICATION_TYPE = findPreference(getString(R.string.SET_NOTIFICATION_TYPE)); + if (SET_NOTIFICATION_TYPE != null) { + SET_NOTIFICATION_TYPE.getContext().setTheme(Helper.dialogStyle()); + } + String[] notificationValues = getResources().getStringArray(R.array.SET_NOTIFICATION_TYPE_VALUE); + if (SET_NOTIFICATION_TYPE != null && SET_NOTIFICATION_TYPE.getValue().equals(notificationValues[2])) { + PreferenceCategory notification_sounds = findPreference("notification_sounds"); + if (notification_sounds != null) { + preferenceScreen.removePreference(notification_sounds); + } + PreferenceCategory notifications_enabled = findPreference("notifications_enabled"); + if (notifications_enabled != null) { + preferenceScreen.removePreference(notifications_enabled); + } + PreferenceCategory notification_time_slot = findPreference("notification_time_slot"); + if (notification_time_slot != null) { + preferenceScreen.removePreference(notification_time_slot); + } + return; + } + + Preference button_mention = findPreference("button_mention"); + assert button_mention != null; + button_mention.setOnPreferenceClickListener(preference -> { + openSettings("channel_mention", getString(R.string.channel_notif_mention)); + return false; + }); + Preference button_follow = findPreference("button_follow"); + assert button_follow != null; + button_follow.setOnPreferenceClickListener(preference -> { + openSettings("channel_follow", getString(R.string.channel_notif_follow)); + return false; + }); + Preference button_reblog = findPreference("button_reblog"); + assert button_reblog != null; + button_reblog.setOnPreferenceClickListener(preference -> { + openSettings("channel_boost", getString(R.string.channel_notif_boost)); + return false; + }); + Preference button_favourite = findPreference("button_favourite"); + assert button_favourite != null; + button_favourite.setOnPreferenceClickListener(preference -> { + openSettings("channel_favourite", getString(R.string.channel_notif_fav)); + return false; + }); + Preference button_poll = findPreference("button_poll"); + assert button_poll != null; + button_poll.setOnPreferenceClickListener(preference -> { + openSettings("channel_poll", getString(R.string.channel_notif_poll)); + return false; + }); + Preference button_status = findPreference("button_status"); + assert button_status != null; + button_status.setOnPreferenceClickListener(preference -> { + openSettings("channel_status", getString(R.string.channel_notif_status)); + return false; + }); + Preference button_media = findPreference("button_media"); + assert button_media != null; + button_media.setOnPreferenceClickListener(preference -> { + openSettings("channel_media", getString(R.string.channel_notif_media)); + return false; + }); + } + + private void createNotificationChannel(String name, String description) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + int importance = NotificationManager.IMPORTANCE_DEFAULT; + NotificationChannel channel = new NotificationChannel(name, name, importance); + channel.setDescription(description); + NotificationManager notificationManager = requireActivity().getSystemService(NotificationManager.class); + notificationManager.createNotificationChannel(channel); + } + } + + private void openSettings(@NonNull String channel, String description) { + + createNotificationChannel(channel, description); + Intent intent; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + intent = new Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS); + intent.putExtra(Settings.EXTRA_APP_PACKAGE, requireActivity().getPackageName()); + intent.putExtra(Settings.EXTRA_CHANNEL_ID, channel); + intent.addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK); + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + intent = new Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS); + intent.putExtra(Settings.EXTRA_APP_PACKAGE, requireActivity().getPackageName()); + intent.putExtra(Settings.EXTRA_CHANNEL_ID, channel); + } else { + intent = new Intent(); + intent.setAction("android.settings.APP_NOTIFICATION_SETTINGS"); + intent.putExtra("app_package", requireActivity().getPackageName()); + intent.putExtra("app_uid", requireActivity().getApplicationInfo().uid); + } + startActivity(intent); + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + if (getActivity() != null) { + if (key.compareToIgnoreCase(getString(R.string.SET_NOTIFICATION_TYPE)) == 0) { + createPref(); + PushHelper.startStreaming(requireActivity()); + } + if (key.compareToIgnoreCase(getString(R.string.SET_LED_COLOUR_VAL)) == 0) { + sharedPreferences.edit().putInt(getString(R.string.SET_LED_COLOUR_VAL), Integer.parseInt(key)).apply(); + } + } + } + + @Override + public void onResume() { + super.onResume(); + + getPreferenceScreen().getSharedPreferences() + .registerOnSharedPreferenceChangeListener(this); + } + + @Override + public void onPause() { + super.onPause(); + getPreferenceScreen().getSharedPreferences() + .unregisterOnSharedPreferenceChangeListener(this); + } + + +} diff --git a/app/src/main/java/app/fedilab/android/ui/fragment/settings/FragmentPrivacySettings.java b/app/src/main/java/app/fedilab/android/ui/fragment/settings/FragmentPrivacySettings.java new file mode 100644 index 000000000..aa8be58f8 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/ui/fragment/settings/FragmentPrivacySettings.java @@ -0,0 +1,63 @@ +package app.fedilab.android.ui.fragment.settings; +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + + +import android.content.SharedPreferences; +import android.os.Bundle; + +import androidx.preference.EditTextPreference; +import androidx.preference.PreferenceFragmentCompat; + +import app.fedilab.android.R; +import app.fedilab.android.helper.Helper; + +public class FragmentPrivacySettings extends PreferenceFragmentCompat implements SharedPreferences.OnSharedPreferenceChangeListener { + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + addPreferencesFromResource(R.xml.pref_privacy); + createPref(); + } + + private void createPref() { + EditTextPreference SET_INVIDIOUS_HOST = findPreference(getString(R.string.SET_INVIDIOUS_HOST)); + if (SET_INVIDIOUS_HOST != null) { + SET_INVIDIOUS_HOST.getContext().setTheme(Helper.dialogStyle()); + } + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + + } + + @Override + public void onResume() { + super.onResume(); + + getPreferenceScreen().getSharedPreferences() + .registerOnSharedPreferenceChangeListener(this); + } + + @Override + public void onPause() { + super.onPause(); + getPreferenceScreen().getSharedPreferences() + .unregisterOnSharedPreferenceChangeListener(this); + } + + +} diff --git a/app/src/main/java/app/fedilab/android/ui/fragment/settings/FragmentThemingSettings.java b/app/src/main/java/app/fedilab/android/ui/fragment/settings/FragmentThemingSettings.java new file mode 100644 index 000000000..446894448 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/ui/fragment/settings/FragmentThemingSettings.java @@ -0,0 +1,583 @@ +package app.fedilab.android.ui.fragment.settings; +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import static android.app.Activity.RESULT_OK; + +import android.Manifest; +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.os.Bundle; +import android.os.Environment; +import android.view.View; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.LinearLayout; +import android.widget.Toast; + +import androidx.appcompat.app.AlertDialog; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; +import androidx.preference.ListPreference; +import androidx.preference.Preference; +import androidx.preference.PreferenceCategory; +import androidx.preference.PreferenceFragmentCompat; +import androidx.preference.PreferenceManager; + +import com.bumptech.glide.Glide; +import com.jaredrummler.cyanea.Cyanea; +import com.jaredrummler.cyanea.prefs.CyaneaSettingsActivity; +import com.jaredrummler.cyanea.prefs.CyaneaTheme; + +import java.io.BufferedReader; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.List; + +import app.fedilab.android.BaseMainActivity; +import app.fedilab.android.R; +import app.fedilab.android.activities.ComposeActivity; +import app.fedilab.android.databinding.PopupStatusThemeBinding; +import app.fedilab.android.helper.Helper; +import app.fedilab.android.helper.ThemeHelper; +import es.dmoral.toasty.Toasty; + +public class FragmentThemingSettings extends PreferenceFragmentCompat implements SharedPreferences.OnSharedPreferenceChangeListener { + + + private final int PICK_IMPORT_THEME = 5557; + private List> listOfThemes; + private SharedPreferences appPref; + private SharedPreferences cyneaPref; + + @Override + public void onCreatePreferences(Bundle bundle, String s) { + cyneaPref = requireActivity().getSharedPreferences("com.jaredrummler.cyanea", Context.MODE_PRIVATE); + appPref = PreferenceManager.getDefaultSharedPreferences(requireActivity()); + createPref(); + listOfThemes = ThemeHelper.getContributorsTheme(requireActivity()); + } + + + @Override + public void onResume() { + super.onResume(); + if (getPreferenceScreen() != null && getPreferenceScreen().getSharedPreferences() != null) { + getPreferenceScreen().getSharedPreferences() + .registerOnSharedPreferenceChangeListener(this); + } + } + + @Override + public void onPause() { + super.onPause(); + if (getPreferenceScreen() != null && getPreferenceScreen().getSharedPreferences() != null) { + getPreferenceScreen().getSharedPreferences() + .unregisterOnSharedPreferenceChangeListener(this); + } + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + + if (key.equals("use_custom_theme")) { + createPref(); + } + ThemeHelper.recreateMainActivity(requireActivity()); + } + + + + + @SuppressWarnings("deprecation") + @SuppressLint("ApplySharedPref") + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (requestCode == PICK_IMPORT_THEME && resultCode == RESULT_OK) { + if (data == null || data.getData() == null) { + Toasty.error(requireActivity(), getString(R.string.theme_file_error), Toast.LENGTH_LONG).show(); + return; + } + if (data.getData() != null) { + try { + InputStream inputStream = requireActivity().getContentResolver().openInputStream(data.getData()); + readFileAndApply(inputStream); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } + } else { + Toasty.error(requireActivity(), getString(R.string.theme_file_error), Toast.LENGTH_LONG).show(); + } + + } + } + + + @SuppressLint("SetTextI18n") + @SuppressWarnings("ConstantConditions") + private void applyColors(PopupStatusThemeBinding binding, int position) { + LinkedHashMap themeData = listOfThemes.get(position); + int linksColor = -1; + int iconsColor = -1; + int textColor = -1; + int boostHeaderColor = -1; + int statusColor = -1; + int displayNameColor = -1; + int userNameColor = -1; + int colorAccent = -1; + int backgroundColor = -1; + if (themeData.containsKey("theme_link_color")) { + linksColor = Integer.parseInt(themeData.get("theme_link_color")); + } + if (themeData.containsKey("theme_accent")) { + colorAccent = Integer.parseInt(themeData.get("theme_accent")); + } + if (themeData.containsKey("theme_icons_color")) { + iconsColor = Integer.parseInt(themeData.get("theme_icons_color")); + } + if (themeData.containsKey("theme_text_color")) { + textColor = Integer.parseInt(themeData.get("theme_text_color")); + } + if (themeData.containsKey("theme_boost_header_color")) { + boostHeaderColor = Integer.parseInt(themeData.get("theme_boost_header_color")); + } + if (themeData.containsKey("theme_statuses_color")) { + statusColor = Integer.parseInt(themeData.get("theme_statuses_color")); + } + if (themeData.containsKey("theme_text_header_1_line")) { + displayNameColor = Integer.parseInt(themeData.get("theme_text_header_1_line")); + } + if (themeData.containsKey("theme_text_header_2_line")) { + userNameColor = Integer.parseInt(themeData.get("theme_text_header_2_line")); + } + if (themeData.containsKey("pref_color_background")) { + backgroundColor = Integer.parseInt(themeData.get("pref_color_background")); + } + + if (colorAccent != -1) { + binding.spoilerExpand.setTextColor(colorAccent); + binding.cardTitle.setTextColor(colorAccent); + } + if (backgroundColor != -1) { + binding.background.setBackgroundColor(backgroundColor); + } + if (statusColor != -1) { + binding.cardviewContainer.setBackgroundColor(statusColor); + binding.card.setBackgroundColor(statusColor); + } + if (boostHeaderColor != -1) { + binding.headerContainer.setBackgroundColor(boostHeaderColor); + } + if (textColor != -1) { + binding.statusContent.setTextColor(textColor); + binding.statusContentTranslated.setTextColor(textColor); + binding.spoiler.setTextColor(textColor); + binding.cardDescription.setTextColor(textColor); + binding.time.setTextColor(textColor); + binding.reblogsCount.setTextColor(textColor); + binding.favoritesCount.setTextColor(textColor); + Helper.changeDrawableColor(requireActivity(), binding.repeatInfo, textColor); + Helper.changeDrawableColor(requireActivity(), binding.favInfo, textColor); + } + if (linksColor != -1) { + binding.cardUrl.setTextColor(linksColor); + } else { + binding.cardUrl.setTextColor(ThemeHelper.getAttColor(requireActivity(), R.attr.linkColor)); + } + if (iconsColor == -1) { + iconsColor = ThemeHelper.getAttColor(requireActivity(), R.attr.iconColor); + } + Helper.changeDrawableColor(requireActivity(), binding.actionButtonReply, iconsColor); + Helper.changeDrawableColor(requireActivity(), binding.actionButtonMore, iconsColor); + Helper.changeDrawableColor(requireActivity(), binding.actionButtonBoost, iconsColor); + Helper.changeDrawableColor(requireActivity(), binding.actionButtonFavorite, iconsColor); + Helper.changeDrawableColor(requireActivity(), R.drawable.ic_person, iconsColor); + if (displayNameColor != -1) { + binding.displayName.setTextColor(displayNameColor); + } else { + binding.displayName.setTextColor(ThemeHelper.getAttColor(requireActivity(), R.attr.statusTextColor)); + } + if (userNameColor != -1) { + binding.username.setTextColor(userNameColor); + Helper.changeDrawableColor(requireActivity(), binding.statusBoostIcon, userNameColor); + } else { + binding.username.setTextColor(ThemeHelper.getAttColor(requireActivity(), R.attr.statusTextColor)); + Helper.changeDrawableColor(requireActivity(), binding.statusBoostIcon, ThemeHelper.getAttColor(requireActivity(), R.attr.statusTextColor)); + } + Glide.with(binding.getRoot().getContext()) + .load(R.drawable.fedilab_logo_bubbles) + .into(binding.statusBoosterAvatar); + Glide.with(binding.getRoot().getContext()) + .load(R.drawable.fedilab_logo_bubbles) + .into(binding.avatar); + binding.displayName.setText("Fedilab"); + binding.username.setText("@apps@toot.fedilab.app"); + + binding.author.setText(themeData.get("author")); + binding.title.setText(themeData.get("name")); + binding.cardviewContainer.invalidate(); + binding.time.setText(Helper.dateToString(new Date())); + } + + @SuppressLint("ApplySharedPref") + private void readFileAndApply(InputStream inputStream) { + BufferedReader br = null; + try { + br = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)); + String sCurrentLine; + SharedPreferences.Editor appEditor = appPref.edit(); + Cyanea.Editor cyaneaEditor = Cyanea.getInstance().edit(); + appEditor.putBoolean("use_custom_theme", true); + while ((sCurrentLine = br.readLine()) != null) { + String[] line = sCurrentLine.split(","); + if (line.length > 1) { + String key = line[0]; + String value = line[1]; + if (key.compareTo("pref_color_navigation_bar") == 0) { + cyaneaEditor.shouldTintNavBar(Boolean.parseBoolean(value)); + } else if (key.compareTo("pref_color_background") == 0) { + cyaneaEditor.backgroundDarkLighter(Integer.parseInt(value)); + cyaneaEditor.backgroundLightDarker(Integer.parseInt(value)); + cyaneaEditor.backgroundDark(Integer.parseInt(value)); + cyaneaEditor.backgroundLightLighter(Integer.parseInt(value)); + cyaneaEditor.backgroundDarkDarker(Integer.parseInt(value)); + cyaneaEditor.background(Integer.parseInt(value)); + cyaneaEditor.backgroundDark(Integer.parseInt(value)); + cyaneaEditor.backgroundLight(Integer.parseInt(value)); + } else if (key.compareTo("base_theme") == 0) { + List list = CyaneaTheme.Companion.from(requireActivity().getAssets(), "themes/cyanea_themes.json"); + CyaneaTheme theme = list.get(Integer.parseInt(value)); + cyaneaEditor.baseTheme(theme.getBaseTheme()); + if (Integer.parseInt(value) == 0 || Integer.parseInt(value) == 2) { + cyaneaEditor.menuIconColor(ContextCompat.getColor(requireActivity(), R.color.dark_text)); + cyaneaEditor.subMenuIconColor(ContextCompat.getColor(requireActivity(), R.color.dark_text)); + } else { + cyaneaEditor.menuIconColor(ContextCompat.getColor(requireActivity(), R.color.black)); + cyaneaEditor.subMenuIconColor(ContextCompat.getColor(requireActivity(), R.color.black)); + } + } else if (key.compareTo("theme_accent") == 0) { + cyaneaEditor.accentLight(Integer.parseInt(value)); + cyaneaEditor.accent(Integer.parseInt(value)); + cyaneaEditor.accentDark(Integer.parseInt(value)); + } else if (key.compareTo("theme_primary") == 0) { + cyaneaEditor.primary(Integer.parseInt(value)); + cyaneaEditor.primaryLight(Integer.parseInt(value)); + cyaneaEditor.primaryDark(Integer.parseInt(value)); + } else { + if (value != null && value.matches("-?\\d+")) { + appEditor.putInt(key, Integer.parseInt(value)); + } else { + appEditor.remove(key); + } + } + } + } + appEditor.commit(); + cyaneaEditor.apply().recreate(requireActivity()); + ThemeHelper.recreateMainActivity(requireActivity()); + } catch (IOException e) { + e.printStackTrace(); + } finally { + try { + if (br != null) br.close(); + } catch (IOException ex) { + ex.printStackTrace(); + } + } + } + + @SuppressWarnings("deprecation") + private void createPref() { + if (getPreferenceScreen() != null) { + getPreferenceScreen().removeAll(); + } + addPreferencesFromResource(R.xml.pref_theming); + if (getPreferenceScreen() == null) { + Toasty.error(requireActivity(), getString(R.string.toast_error), Toasty.LENGTH_SHORT).show(); + return; + } + Preference launch_custom_theme = findPreference("launch_custom_theme"); + if (launch_custom_theme != null) { + launch_custom_theme.setOnPreferenceClickListener(preference -> { + startActivity(new Intent(requireActivity(), CyaneaSettingsActivity.class)); + return false; + }); + + } + Preference contributors_themes = findPreference("contributors_themes"); + if (contributors_themes != null) { + contributors_themes.setOnPreferenceClickListener(preference -> { + final int[] currentPosition = {0}; + AlertDialog.Builder builderSingle = new AlertDialog.Builder(requireActivity(), Helper.dialogStyle()); + builderSingle.setTitle(getString(R.string.select_a_theme)); + PopupStatusThemeBinding binding = PopupStatusThemeBinding.inflate(getLayoutInflater(), new LinearLayout(requireActivity()), false); + binding.selectTheme.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView adapterView, View view, int position, long l) { + currentPosition[0] = position; + applyColors(binding, position); + } + + @Override + public void onNothingSelected(AdapterView adapterView) { + + } + }); + applyColors(binding, 0); + builderSingle.setView(binding.getRoot()); + String[] listOfTheme = new String[listOfThemes.size()]; + int i = 0; + for (LinkedHashMap values : listOfThemes) { + listOfTheme[i] = values.get("name"); + i++; + } + //fill data in spinner + ArrayAdapter adapter = new ArrayAdapter<>(requireActivity(), android.R.layout.simple_spinner_dropdown_item, listOfTheme); + binding.selectTheme.setAdapter(adapter); + binding.selectTheme.setSelection(0); + builderSingle.setPositiveButton(R.string.validate, (dialog, which) -> { + try { + String[] list = requireActivity().getAssets().list("themes/contributors"); + InputStream is = requireActivity().getAssets().open("themes/contributors/" + list[currentPosition[0]]); + readFileAndApply(is); + } catch (IOException e) { + e.printStackTrace(); + } + dialog.dismiss(); + }); + builderSingle.setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss()); + builderSingle.show(); + return false; + }); + + } + + ListPreference settings_theme = findPreference("settings_theme"); + if (settings_theme != null) { + settings_theme.getContext().setTheme(Helper.dialogStyle()); + } + + Preference theme_link_color = findPreference("theme_link_color"); + Preference theme_boost_header_color = findPreference("theme_boost_header_color"); + Preference theme_text_header_1_line = findPreference("theme_text_header_1_line"); + Preference theme_text_header_2_line = findPreference("theme_text_header_2_line"); + Preference theme_statuses_color = findPreference("theme_statuses_color"); + Preference theme_icons_color = findPreference("theme_icons_color"); + Preference theme_text_color = findPreference("theme_text_color"); + Preference pref_import = findPreference("pref_import"); + Preference pref_export = findPreference("pref_export"); + Preference reset_pref = findPreference("reset_pref"); + PreferenceCategory cyanea_preference_category = getPreferenceScreen().findPreference("cyanea_preference_category"); + //No custom theme data must be removed + if (!appPref.getBoolean("use_custom_theme", false) && cyanea_preference_category != null) { + if (theme_link_color != null) { + cyanea_preference_category.removePreference(theme_link_color); + } + if (theme_boost_header_color != null) { + cyanea_preference_category.removePreference(theme_boost_header_color); + } + if (theme_text_header_1_line != null) { + cyanea_preference_category.removePreference(theme_text_header_1_line); + } + if (theme_text_header_2_line != null) { + cyanea_preference_category.removePreference(theme_text_header_2_line); + } + if (contributors_themes != null) { + cyanea_preference_category.removePreference(contributors_themes); + } + if (theme_statuses_color != null) { + cyanea_preference_category.removePreference(theme_statuses_color); + } + if (theme_icons_color != null) { + cyanea_preference_category.removePreference(theme_icons_color); + } + if (theme_text_color != null) { + cyanea_preference_category.removePreference(theme_text_color); + } + if (reset_pref != null) { + cyanea_preference_category.removePreference(reset_pref); + } + if (pref_export != null) { + cyanea_preference_category.removePreference(pref_export); + } + } + //These are default values (first three ones) + if (pref_export != null) { + pref_export.setOnPreferenceClickListener(preference -> { + exportColors(); + return true; + }); + } + + if (pref_import != null) { + pref_import.setOnPreferenceClickListener(preference -> { + if (ContextCompat.checkSelfPermission(requireActivity(), Manifest.permission.READ_EXTERNAL_STORAGE) != + PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions(requireActivity(), + new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, + ComposeActivity.MY_PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE); + return true; + } + Intent intent = new Intent(Intent.ACTION_GET_CONTENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setType("*/*"); + String[] mimetypes = {"*/*"}; + intent.putExtra(Intent.EXTRA_MIME_TYPES, mimetypes); + startActivityForResult(intent, PICK_IMPORT_THEME); + return true; + }); + } + if (reset_pref != null) { + reset_pref.setOnPreferenceClickListener(preference -> { + AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(requireActivity(), Helper.dialogStyle()); + dialogBuilder.setMessage(R.string.reset_color); + dialogBuilder.setPositiveButton(R.string.reset, (dialog, id) -> { + reset(); + dialog.dismiss(); + setPreferenceScreen(null); + createPref(); + + }); + dialogBuilder.setNegativeButton(R.string.cancel, (dialog, id) -> dialog.dismiss()); + AlertDialog alertDialog = dialogBuilder.create(); + alertDialog.setCancelable(false); + alertDialog.show(); + return true; + }); + } + } + + @SuppressLint("ApplySharedPref") + private void reset() { + + SharedPreferences.Editor editor = appPref.edit(); + editor.remove("theme_link_color"); + editor.remove("theme_boost_header_color"); + editor.remove("theme_text_header_1_line"); + editor.remove("theme_text_header_2_line"); + editor.remove("theme_icons_color"); + editor.remove("theme_text_color"); + editor.remove("use_custom_theme"); + editor.commit(); + } + + + private void exportColors() { + + try { + String fileName = "Fedilab_color_export_" + Helper.dateFileToString(getActivity(), new Date()) + ".csv"; + String filePath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getAbsolutePath(); + String fullPath = filePath + "/" + fileName; + PrintWriter pw = new PrintWriter(new OutputStreamWriter(new FileOutputStream(fullPath), StandardCharsets.UTF_8)); + StringBuilder builder = new StringBuilder(); + int theme_boost_header_color = appPref.getInt("theme_boost_header_color", -1); + int theme_text_header_1_line = appPref.getInt("theme_text_header_1_line", -1); + int theme_text_header_2_line = appPref.getInt("theme_text_header_2_line", -1); + int theme_statuses_color = appPref.getInt("theme_statuses_color", -1); + int theme_link_color = appPref.getInt("theme_link_color", -1); + int theme_icons_color = appPref.getInt("theme_icons_color", -1); + int pref_color_background = cyneaPref.getInt("pref_color_background", -1); + boolean pref_color_navigation_bar = cyneaPref.getBoolean("pref_color_navigation_bar", true); + boolean pref_color_status_bar = cyneaPref.getBoolean("pref_color_status_bar", true); + int theme_accent = cyneaPref.getInt("theme_accent", -1); + int theme_text_color = appPref.getInt("theme_text_color", -1); + int theme_primary = cyneaPref.getInt("theme_primary", -1); + + int theme = appPref.getInt(getString(R.string.SET_THEME), 0); + + + builder.append("base_theme").append(','); + builder.append(theme); + builder.append('\n'); + + builder.append("theme_boost_header_color").append(','); + builder.append(theme_boost_header_color); + builder.append('\n'); + + builder.append("theme_text_header_1_line").append(','); + builder.append(theme_text_header_1_line); + builder.append('\n'); + + builder.append("theme_text_header_2_line").append(','); + builder.append(theme_text_header_2_line); + builder.append('\n'); + + builder.append("theme_statuses_color").append(','); + builder.append(theme_statuses_color); + builder.append('\n'); + + builder.append("theme_link_color").append(','); + builder.append(theme_link_color); + builder.append('\n'); + + builder.append("theme_icons_color").append(','); + builder.append(theme_icons_color); + builder.append('\n'); + + builder.append("pref_color_background").append(','); + builder.append(pref_color_background); + builder.append('\n'); + + builder.append("pref_color_navigation_bar").append(','); + builder.append(pref_color_navigation_bar); + builder.append('\n'); + + builder.append("pref_color_status_bar").append(','); + builder.append(pref_color_status_bar); + builder.append('\n'); + + builder.append("theme_accent").append(','); + builder.append(theme_accent); + builder.append('\n'); + + builder.append("theme_text_color").append(','); + builder.append(theme_text_color); + builder.append('\n'); + + builder.append("theme_primary").append(','); + builder.append(theme_primary); + builder.append('\n'); + + + pw.write(builder.toString()); + pw.close(); + String message = getString(R.string.data_export_theme_success); + Intent intentOpen = new Intent(); + intentOpen.setAction(android.content.Intent.ACTION_VIEW); + Uri uri = Uri.parse("file://" + fullPath); + intentOpen.setDataAndType(uri, "text/csv"); + String title = getString(R.string.data_export_theme); + Helper.notify_user(getActivity(), BaseMainActivity.accountWeakReference.get(), intentOpen, BitmapFactory.decodeResource(requireActivity().getResources(), + R.mipmap.ic_launcher), Helper.NotifType.BACKUP, title, message); + } catch (Exception e) { + e.printStackTrace(); + Toasty.error(requireActivity(), getString(R.string.toast_error), Toast.LENGTH_LONG).show(); + } + } +} diff --git a/app/src/main/java/app/fedilab/android/ui/fragment/settings/FragmentTimelinesSettings.java b/app/src/main/java/app/fedilab/android/ui/fragment/settings/FragmentTimelinesSettings.java new file mode 100644 index 000000000..31eff4b05 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/ui/fragment/settings/FragmentTimelinesSettings.java @@ -0,0 +1,63 @@ +package app.fedilab.android.ui.fragment.settings; +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import android.content.SharedPreferences; +import android.os.Bundle; + +import androidx.preference.PreferenceFragmentCompat; +import androidx.preference.PreferenceManager; + +import app.fedilab.android.R; + +public class FragmentTimelinesSettings extends PreferenceFragmentCompat implements SharedPreferences.OnSharedPreferenceChangeListener { + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + addPreferencesFromResource(R.xml.pref_timelines); + createPref(); + } + + private void createPref() { + + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + if (getActivity() != null) { + SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(requireActivity()); + SharedPreferences.Editor editor = sharedpreferences.edit(); + + editor.apply(); + } + } + + @Override + public void onResume() { + super.onResume(); + + getPreferenceScreen().getSharedPreferences() + .registerOnSharedPreferenceChangeListener(this); + } + + @Override + public void onPause() { + super.onPause(); + getPreferenceScreen().getSharedPreferences() + .unregisterOnSharedPreferenceChangeListener(this); + } + + +} diff --git a/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonAccount.java b/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonAccount.java new file mode 100644 index 000000000..d3977b9f7 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonAccount.java @@ -0,0 +1,270 @@ +package app.fedilab.android.ui.fragment.timeline; +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.ArrayList; +import java.util.List; + +import app.fedilab.android.BaseMainActivity; +import app.fedilab.android.R; +import app.fedilab.android.client.entities.Timeline; +import app.fedilab.android.client.mastodon.entities.Account; +import app.fedilab.android.client.mastodon.entities.Accounts; +import app.fedilab.android.client.mastodon.entities.Pagination; +import app.fedilab.android.client.mastodon.entities.RelationShip; +import app.fedilab.android.databinding.FragmentPaginationBinding; +import app.fedilab.android.helper.Helper; +import app.fedilab.android.helper.MastodonHelper; +import app.fedilab.android.ui.drawer.AccountAdapter; +import app.fedilab.android.ui.pageadapter.FedilabProfileTLPageAdapter; +import app.fedilab.android.viewmodel.mastodon.AccountsVM; +import app.fedilab.android.viewmodel.mastodon.SearchVM; + + +public class FragmentMastodonAccount extends Fragment { + + + private FragmentPaginationBinding binding; + private AccountsVM accountsVM; + private boolean flagLoading; + private List accounts; + private String max_id; + private AccountAdapter accountAdapter; + private String search; + private Account accountTimeline; + private FedilabProfileTLPageAdapter.follow_type followType; + private String viewModelKey; + private Timeline.TimeLineEnum timelineType; + + public View onCreateView(@NonNull LayoutInflater inflater, + ViewGroup container, Bundle savedInstanceState) { + if (getArguments() != null) { + search = getArguments().getString(Helper.ARG_SEARCH_KEYWORD, null); + accountTimeline = (Account) getArguments().getSerializable(Helper.ARG_ACCOUNT); + followType = (FedilabProfileTLPageAdapter.follow_type) getArguments().getSerializable(Helper.ARG_FOLLOW_TYPE); + viewModelKey = getArguments().getString(Helper.ARG_VIEW_MODEL_KEY, ""); + timelineType = (Timeline.TimeLineEnum) getArguments().get(Helper.ARG_TIMELINE_TYPE); + } + + flagLoading = false; + binding = FragmentPaginationBinding.inflate(inflater, container, false); + return binding.getRoot(); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + int c1 = getResources().getColor(R.color.cyanea_accent_reference); + binding.swipeContainer.setProgressBackgroundColorSchemeColor(getResources().getColor(R.color.cyanea_primary_reference)); + binding.swipeContainer.setColorSchemeColors( + c1, c1, c1 + ); + binding.loader.setVisibility(View.VISIBLE); + binding.recyclerView.setVisibility(View.GONE); + accountsVM = new ViewModelProvider(FragmentMastodonAccount.this).get(viewModelKey, AccountsVM.class); + max_id = null; + router(true); + } + + /** + * Router for timelines + */ + private void router(boolean firstLoad) { + if (followType == FedilabProfileTLPageAdapter.follow_type.FOLLOWERS) { + if (firstLoad) { + accountsVM.getAccountFollowers(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, accountTimeline.id, null, null) + .observe(getViewLifecycleOwner(), this::initializeAccountCommonView); + } else { + accountsVM.getAccountFollowers(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, accountTimeline.id, max_id, null) + .observe(getViewLifecycleOwner(), this::dealWithPagination); + } + } else if (followType == FedilabProfileTLPageAdapter.follow_type.FOLLOWING) { + if (firstLoad) { + accountsVM.getAccountFollowing(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, accountTimeline.id, null, null) + .observe(getViewLifecycleOwner(), this::initializeAccountCommonView); + } else { + accountsVM.getAccountFollowing(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, accountTimeline.id, max_id, null) + .observe(getViewLifecycleOwner(), this::dealWithPagination); + } + } else if (search != null) { + SearchVM searchVM = new ViewModelProvider(FragmentMastodonAccount.this).get(viewModelKey, SearchVM.class); + searchVM.search(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, search.trim(), null, "accounts", false, true, false, 0, null, null, MastodonHelper.STATUSES_PER_CALL) + .observe(getViewLifecycleOwner(), results -> { + Accounts accounts = new Accounts(); + Pagination pagination = new Pagination(); + accounts.accounts = results.accounts; + accounts.pagination = pagination; + initializeAccountCommonView(accounts); + }); + } else if (timelineType == Timeline.TimeLineEnum.MUTED_TIMELINE) { + if (firstLoad) { + accountsVM.getMutes(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, String.valueOf(MastodonHelper.accountsPerCall(requireActivity())), null, null) + .observe(getViewLifecycleOwner(), this::initializeAccountCommonView); + } else { + accountsVM.getMutes(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, String.valueOf(MastodonHelper.accountsPerCall(requireActivity())), max_id, null) + .observe(getViewLifecycleOwner(), this::dealWithPagination); + } + } else if (timelineType == Timeline.TimeLineEnum.BLOCKED_TIMELINE) { + if (firstLoad) { + accountsVM.getBlocks(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, String.valueOf(MastodonHelper.accountsPerCall(requireActivity())), null, null) + .observe(getViewLifecycleOwner(), this::initializeAccountCommonView); + } else { + accountsVM.getBlocks(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, String.valueOf(MastodonHelper.accountsPerCall(requireActivity())), max_id, null) + .observe(getViewLifecycleOwner(), this::initializeAccountCommonView); + } + } + } + + private void fetchRelationShip(List accounts, int position) { + List ids = new ArrayList<>(); + for (Account account : accounts) { + ids.add(account.id); + } + accountsVM.getRelationships(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, ids) + .observe(getViewLifecycleOwner(), relationShips -> { + + for (RelationShip relationShip : relationShips) { + for (Account account : accounts) { + if (account.id.compareToIgnoreCase(relationShip.id) == 0) { + account.relationShip = relationShip; + } + } + } + accountAdapter.notifyItemRangeChanged(position, accounts.size()); + }); + } + + + public void scrollToTop() { + binding.recyclerView.setAdapter(accountAdapter); + } + + /** + * Intialize the view for accounts + * + * @param accounts {@link Accounts} + */ + private void initializeAccountCommonView(final Accounts accounts) { + if (binding == null) { + return; + } + binding.loader.setVisibility(View.GONE); + binding.noAction.setVisibility(View.GONE); + binding.swipeContainer.setRefreshing(false); + binding.swipeContainer.setOnRefreshListener(() -> { + binding.swipeContainer.setRefreshing(true); + max_id = null; + router(true); + }); + if (accounts == null || accounts.accounts == null || accounts.accounts.size() == 0) { + binding.noAction.setVisibility(View.VISIBLE); + binding.noActionText.setText(R.string.no_accounts); + return; + } + binding.recyclerView.setVisibility(View.VISIBLE); + if (accountAdapter != null && this.accounts != null) { + int size = this.accounts.size(); + this.accounts.clear(); + this.accounts = new ArrayList<>(); + accountAdapter.notifyItemRangeRemoved(0, size); + } + + this.accounts = accounts.accounts; + accountAdapter = new AccountAdapter(this.accounts); + + LinearLayoutManager mLayoutManager = new LinearLayoutManager(requireActivity()); + binding.recyclerView.setLayoutManager(mLayoutManager); + binding.recyclerView.setAdapter(accountAdapter); + //Fetch the relationship + fetchRelationShip(accounts.accounts, 0); + max_id = accounts.pagination.min_id; + binding.recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { + if (requireActivity() instanceof BaseMainActivity) { + if (dy < 0 && !((BaseMainActivity) requireActivity()).getFloatingVisibility()) + ((BaseMainActivity) requireActivity()).manageFloatingButton(true); + if (dy > 0 && ((BaseMainActivity) requireActivity()).getFloatingVisibility()) + ((BaseMainActivity) requireActivity()).manageFloatingButton(false); + } + int firstVisibleItem = mLayoutManager.findFirstVisibleItemPosition(); + if (dy > 0) { + int visibleItemCount = mLayoutManager.getChildCount(); + int totalItemCount = mLayoutManager.getItemCount(); + if (firstVisibleItem + visibleItemCount == totalItemCount) { + if (!flagLoading) { + flagLoading = true; + binding.loadingNextElements.setVisibility(View.VISIBLE); + router(false); + } + } else { + binding.loadingNextElements.setVisibility(View.GONE); + } + } + + } + }); + } + + + /** + * Update view and pagination when scrolling down + * + * @param fetched_accounts Accounts + */ + private void dealWithPagination(Accounts fetched_accounts) { + flagLoading = false; + if (binding == null) { + return; + } + binding.loadingNextElements.setVisibility(View.GONE); + if (accounts != null && fetched_accounts != null && fetched_accounts.accounts != null) { + int startId = 0; + //There are some statuses present in the timeline + if (accounts.size() > 0) { + startId = accounts.size(); + } + int position = accounts.size(); + accounts.addAll(fetched_accounts.accounts); + //Fetch the relationship + fetchRelationShip(fetched_accounts.accounts, position); + max_id = fetched_accounts.pagination.min_id; + accountAdapter.notifyItemRangeInserted(startId, fetched_accounts.accounts.size()); + } + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + if (binding != null) { + binding.recyclerView.setAdapter(null); + } + accountAdapter = null; + binding = null; + } +} \ No newline at end of file diff --git a/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonContext.java b/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonContext.java new file mode 100644 index 000000000..74d07af9e --- /dev/null +++ b/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonContext.java @@ -0,0 +1,163 @@ +package app.fedilab.android.ui.fragment.timeline; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import static app.fedilab.android.activities.ContextActivity.expand; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; +import androidx.recyclerview.widget.LinearLayoutManager; + +import java.util.ArrayList; +import java.util.List; + +import app.fedilab.android.BaseMainActivity; +import app.fedilab.android.R; +import app.fedilab.android.client.mastodon.entities.Context; +import app.fedilab.android.client.mastodon.entities.Status; +import app.fedilab.android.databinding.FragmentPaginationBinding; +import app.fedilab.android.helper.DividerDecoration; +import app.fedilab.android.helper.Helper; +import app.fedilab.android.ui.drawer.StatusAdapter; +import app.fedilab.android.viewmodel.mastodon.StatusesVM; + + +public class FragmentMastodonContext extends Fragment { + + + private FragmentPaginationBinding binding; + private StatusesVM statusesVM; + private List statuses; + private StatusAdapter statusAdapter; + private Status focusedStatus; + private Status firstStatus; + private boolean pullToRefresh; + + public View onCreateView(@NonNull LayoutInflater inflater, + ViewGroup container, Bundle savedInstanceState) { + + focusedStatus = null; + pullToRefresh = false; + if (getArguments() != null) { + focusedStatus = (Status) getArguments().getSerializable(Helper.ARG_STATUS); + } + if (focusedStatus == null) { + requireActivity().getSupportFragmentManager().beginTransaction().remove(this).commit(); + } + binding = FragmentPaginationBinding.inflate(inflater, container, false); + int c1 = getResources().getColor(R.color.cyanea_accent_reference); + binding.swipeContainer.setProgressBackgroundColorSchemeColor(getResources().getColor(R.color.cyanea_primary_reference)); + binding.swipeContainer.setColorSchemeColors( + c1, c1, c1 + ); + + statusesVM = new ViewModelProvider(FragmentMastodonContext.this).get(StatusesVM.class); + binding.recyclerView.setNestedScrollingEnabled(true); + this.statuses = new ArrayList<>(); + focusedStatus.isFocused = true; + this.statuses.add(focusedStatus); + statusAdapter = new StatusAdapter(this.statuses, false); + binding.swipeContainer.setRefreshing(false); + LinearLayoutManager mLayoutManager = new LinearLayoutManager(requireActivity()); + binding.recyclerView.setLayoutManager(mLayoutManager); + binding.recyclerView.setAdapter(statusAdapter); + binding.swipeContainer.setOnRefreshListener(() -> { + if (this.statuses.size() > 0) { + binding.swipeContainer.setRefreshing(true); + pullToRefresh = true; + statusesVM.getContext(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, focusedStatus.id) + .observe(getViewLifecycleOwner(), this::initializeContextView); + } + }); + if (focusedStatus != null) { + statusesVM.getContext(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, focusedStatus.id) + .observe(getViewLifecycleOwner(), this::initializeContextView); + } + return binding.getRoot(); + } + + public void refresh() { + if (statusAdapter != null && statuses != null) { + statusAdapter.notifyItemRangeChanged(0, statuses.size()); + } + } + + public void redraw() { + if (statusAdapter != null && firstStatus != null) { + pullToRefresh = true; + String id; + if (expand) + id = firstStatus.id; + else + id = focusedStatus.id; + statusesVM.getContext(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, id) + .observe(FragmentMastodonContext.this, this::initializeContextView); + } + } + + + /** + * Intialize the common view for the context + * + * @param context {@link Context} + */ + private void initializeContextView(final Context context) { + + if (context == null) { + return; + } + if (pullToRefresh) { + pullToRefresh = false; + int size = this.statuses.size(); + statuses.clear(); + statusAdapter.notifyItemRangeRemoved(0, size); + statuses.add(focusedStatus); + } + if (context.ancestors.size() > 0) { + firstStatus = context.ancestors.get(0); + } else { + firstStatus = statuses.get(0); + } + int statusPosition = context.ancestors.size(); + //Build the array of statuses + statuses.addAll(0, context.ancestors); + statusAdapter.notifyItemRangeInserted(0, statusPosition); + statuses.addAll(statusPosition + 1, context.descendants); + statusAdapter.notifyItemRangeInserted(statusPosition + 1, context.descendants.size()); + if (binding.recyclerView.getItemDecorationCount() > 0) { + for (int i = 0; i < binding.recyclerView.getItemDecorationCount(); i++) { + binding.recyclerView.removeItemDecorationAt(i); + } + } + binding.recyclerView.addItemDecoration(new DividerDecoration(requireActivity(), statuses)); + binding.swipeContainer.setRefreshing(false); + binding.recyclerView.scrollToPosition(statusPosition); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + binding.recyclerView.setAdapter(null); + statusAdapter = null; + binding = null; + } + +} \ No newline at end of file diff --git a/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonConversation.java b/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonConversation.java new file mode 100644 index 000000000..870ada87e --- /dev/null +++ b/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonConversation.java @@ -0,0 +1,159 @@ +package app.fedilab.android.ui.fragment.timeline; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.ArrayList; +import java.util.List; + +import app.fedilab.android.BaseMainActivity; +import app.fedilab.android.R; +import app.fedilab.android.client.mastodon.entities.Conversation; +import app.fedilab.android.client.mastodon.entities.Conversations; +import app.fedilab.android.databinding.FragmentPaginationBinding; +import app.fedilab.android.helper.MastodonHelper; +import app.fedilab.android.ui.drawer.ConversationAdapter; +import app.fedilab.android.viewmodel.mastodon.TimelinesVM; + + +public class FragmentMastodonConversation extends Fragment { + + + private FragmentPaginationBinding binding; + private TimelinesVM timelinesVM; + private FragmentMastodonConversation currentFragment; + private boolean flagLoading; + private List conversations; + private String max_id; + private ConversationAdapter conversationAdapter; + + public View onCreateView(@NonNull LayoutInflater inflater, + ViewGroup container, Bundle savedInstanceState) { + currentFragment = this; + flagLoading = false; + binding = FragmentPaginationBinding.inflate(inflater, container, false); + return binding.getRoot(); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + int c1 = getResources().getColor(R.color.cyanea_accent_reference); + binding.swipeContainer.setProgressBackgroundColorSchemeColor(getResources().getColor(R.color.cyanea_primary_reference)); + binding.swipeContainer.setColorSchemeColors( + c1, c1, c1 + ); + binding.loader.setVisibility(View.VISIBLE); + binding.recyclerView.setVisibility(View.GONE); + timelinesVM = new ViewModelProvider(FragmentMastodonConversation.this).get(TimelinesVM.class); + max_id = null; + timelinesVM.getConversations(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, null, null, null, MastodonHelper.statusesPerCall(requireActivity())) + .observe(getViewLifecycleOwner(), this::initializeConversationCommonView); + } + + /** + * Intialize the view for conversations + * + * @param conversations {@link Conversations} + */ + private void initializeConversationCommonView(final Conversations conversations) { + + binding.loader.setVisibility(View.GONE); + binding.noAction.setVisibility(View.GONE); + if (conversationAdapter != null && this.conversations != null) { + int size = this.conversations.size(); + this.conversations.clear(); + this.conversations = new ArrayList<>(); + conversationAdapter.notifyItemRangeRemoved(0, size); + } + binding.recyclerView.setVisibility(View.VISIBLE); + this.conversations = conversations.conversations; + conversationAdapter = new ConversationAdapter(this.conversations); + //conversationAdapter.itemListener = currentFragment; + binding.swipeContainer.setRefreshing(false); + LinearLayoutManager mLayoutManager = new LinearLayoutManager(requireActivity()); + binding.recyclerView.setLayoutManager(mLayoutManager); + binding.recyclerView.setAdapter(conversationAdapter); + max_id = conversations.pagination.min_id; + binding.recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { + int firstVisibleItem = mLayoutManager.findFirstVisibleItemPosition(); + if (dy > 0) { + int visibleItemCount = mLayoutManager.getChildCount(); + int totalItemCount = mLayoutManager.getItemCount(); + if (firstVisibleItem + visibleItemCount == totalItemCount) { + if (!flagLoading) { + flagLoading = true; + binding.loadingNextElements.setVisibility(View.VISIBLE); + timelinesVM.getConversations(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, max_id, null, null, MastodonHelper.statusesPerCall(requireActivity())) + .observe(FragmentMastodonConversation.this, fetched_conversations -> { + flagLoading = false; + binding.loadingNextElements.setVisibility(View.GONE); + if (currentFragment.conversations != null && fetched_conversations != null) { + int startId = 0; + //There are some statuses present in the timeline + if (currentFragment.conversations.size() > 0) { + startId = currentFragment.conversations.size(); + } + currentFragment.conversations.addAll(fetched_conversations.conversations); + max_id = fetched_conversations.pagination.min_id; + conversationAdapter.notifyItemRangeInserted(startId, fetched_conversations.conversations.size()); + } + }); + } + } else { + binding.loadingNextElements.setVisibility(View.GONE); + } + } + + } + }); + binding.swipeContainer.setOnRefreshListener(() -> { + if (this.conversations.size() > 0) { + binding.swipeContainer.setRefreshing(true); + max_id = null; + timelinesVM.getConversations(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, null, null, null, MastodonHelper.statusesPerCall(requireActivity())) + .observe(FragmentMastodonConversation.this, this::initializeConversationCommonView); + } + }); + + } + + public void scrollToTop() { + binding.recyclerView.scrollToPosition(0); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + binding.recyclerView.setAdapter(null); + conversationAdapter = null; + binding = null; + } +} \ No newline at end of file diff --git a/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonNotification.java b/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonNotification.java new file mode 100644 index 000000000..68d60e8bd --- /dev/null +++ b/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonNotification.java @@ -0,0 +1,240 @@ +package app.fedilab.android.ui.fragment.timeline; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import android.content.SharedPreferences; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; +import androidx.preference.PreferenceManager; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.google.gson.annotations.SerializedName; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import app.fedilab.android.BaseMainActivity; +import app.fedilab.android.R; +import app.fedilab.android.client.mastodon.entities.Notification; +import app.fedilab.android.client.mastodon.entities.Notifications; +import app.fedilab.android.databinding.FragmentPaginationBinding; +import app.fedilab.android.helper.Helper; +import app.fedilab.android.helper.MastodonHelper; +import app.fedilab.android.ui.drawer.NotificationAdapter; +import app.fedilab.android.viewmodel.mastodon.NotificationsVM; + + +public class FragmentMastodonNotification extends Fragment { + + + private FragmentPaginationBinding binding; + private NotificationsVM notificationsVM; + private FragmentMastodonNotification currentFragment; + private boolean flagLoading; + private List notifications; + private String max_id; + private NotificationAdapter notificationAdapter; + private NotificationTypeEnum notificationType; + private List excludeType; + + public View onCreateView(@NonNull LayoutInflater inflater, + ViewGroup container, Bundle savedInstanceState) { + + currentFragment = this; + flagLoading = false; + binding = FragmentPaginationBinding.inflate(inflater, container, false); + View root = binding.getRoot(); + if (getArguments() != null) { + notificationType = (NotificationTypeEnum) getArguments().get(Helper.ARG_NOTIFICATION_TYPE); + } + SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(requireActivity()); + String excludedCategories = sharedpreferences.getString(getString(R.string.SET_EXCLUDED_NOTIFICATIONS_TYPE) + BaseMainActivity.currentUserID + BaseMainActivity.currentInstance, null); + int c1 = getResources().getColor(R.color.cyanea_accent_reference); + binding.swipeContainer.setProgressBackgroundColorSchemeColor(getResources().getColor(R.color.cyanea_primary_reference)); + binding.swipeContainer.setColorSchemeColors( + c1, c1, c1 + ); + notificationsVM = new ViewModelProvider(FragmentMastodonNotification.this).get(NotificationsVM.class); + binding.loader.setVisibility(View.VISIBLE); + binding.recyclerView.setVisibility(View.GONE); + max_id = null; + excludeType = new ArrayList<>(); + excludeType.add("follow"); + excludeType.add("favourite"); + excludeType.add("reblog"); + excludeType.add("poll"); + excludeType.add("follow_request"); + excludeType.add("mention"); + excludeType.add("update"); + excludeType.add("status"); + if (notificationType == NotificationTypeEnum.ALL) { + if (excludedCategories != null) { + excludeType = new ArrayList<>(); + String[] categoriesArray = excludedCategories.split("\\|"); + Collections.addAll(excludeType, categoriesArray); + } else { + excludeType = null; + } + } else if (notificationType == NotificationTypeEnum.MENTIONS) { + excludeType.remove("mention"); + } else if (notificationType == NotificationTypeEnum.FAVOURITES) { + excludeType.remove("favourite"); + } else if (notificationType == NotificationTypeEnum.REBLOGS) { + excludeType.remove("reblog"); + } else if (notificationType == NotificationTypeEnum.POLLS) { + excludeType.remove("poll"); + } else if (notificationType == NotificationTypeEnum.TOOTS) { + excludeType.remove("status"); + } else if (notificationType == NotificationTypeEnum.FOLLOWS) { + excludeType.remove("follow"); + excludeType.remove("follow_request"); + } + notificationsVM.getNotifications(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, null, null, null, MastodonHelper.statusesPerCall(requireActivity()), excludeType, null) + .observe(getViewLifecycleOwner(), this::initializeNotificationView); + return root; + } + + /** + * Intialize the view for notifications + * + * @param notifications {@link app.fedilab.android.client.mastodon.entities.Notifications} + */ + private void initializeNotificationView(final Notifications notifications) { + + binding.loader.setVisibility(View.GONE); + binding.swipeContainer.setRefreshing(false); + if (notifications == null || notifications.notifications == null) { + binding.noActionText.setText(R.string.no_notifications); + binding.noAction.setVisibility(View.VISIBLE); + binding.recyclerView.setVisibility(View.GONE); + return; + } else { + binding.noAction.setVisibility(View.GONE); + binding.recyclerView.setVisibility(View.VISIBLE); + } + if (notificationAdapter != null && this.notifications != null) { + int size = this.notifications.size(); + this.notifications.clear(); + this.notifications = new ArrayList<>(); + notificationAdapter.notifyItemRangeRemoved(0, size); + } + this.notifications = notifications.notifications; + notificationAdapter = new NotificationAdapter(this.notifications); + + LinearLayoutManager mLayoutManager = new LinearLayoutManager(requireActivity()); + binding.recyclerView.setLayoutManager(mLayoutManager); + binding.recyclerView.setAdapter(notificationAdapter); + max_id = notifications.pagination.min_id; + binding.recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { + int firstVisibleItem = mLayoutManager.findFirstVisibleItemPosition(); + if (dy > 0) { + int visibleItemCount = mLayoutManager.getChildCount(); + int totalItemCount = mLayoutManager.getItemCount(); + if (firstVisibleItem + visibleItemCount == totalItemCount) { + if (!flagLoading) { + flagLoading = true; + binding.loadingNextElements.setVisibility(View.VISIBLE); + notificationsVM.getNotifications(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, max_id, null, null, MastodonHelper.statusesPerCall(requireActivity()), excludeType, null) + .observe(FragmentMastodonNotification.this, fetched_notifications -> dealWithPagination(fetched_notifications)); + } + } else { + binding.loadingNextElements.setVisibility(View.GONE); + } + } + + } + }); + binding.swipeContainer.setOnRefreshListener(() -> { + if (this.notifications.size() > 0) { + binding.swipeContainer.setRefreshing(true); + max_id = null; + notificationsVM.getNotifications(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, null, null, null, MastodonHelper.statusesPerCall(requireActivity()), excludeType, null) + .observe(FragmentMastodonNotification.this, this::initializeNotificationView); + } + }); + + } + + public void scrollToTop() { + binding.recyclerView.scrollToPosition(0); + } + + /** + * Update view and pagination when scrolling down + * + * @param fetched_notifications Notifications + */ + private void dealWithPagination(Notifications fetched_notifications) { + flagLoading = false; + binding.loadingNextElements.setVisibility(View.GONE); + if (currentFragment.notifications != null && fetched_notifications != null && fetched_notifications.notifications != null) { + int startId = 0; + //There are some statuses present in the timeline + if (currentFragment.notifications.size() > 0) { + startId = currentFragment.notifications.size(); + } + currentFragment.notifications.addAll(fetched_notifications.notifications); + max_id = fetched_notifications.pagination.min_id; + notificationAdapter.notifyItemRangeInserted(startId, fetched_notifications.notifications.size()); + } + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + binding.recyclerView.setAdapter(null); + notificationAdapter = null; + binding = null; + } + + + public enum NotificationTypeEnum { + @SerializedName("ALL") + ALL("ALL"), + @SerializedName("MENTIONS") + MENTIONS("MENTIONS"), + @SerializedName("FAVOURITES") + FAVOURITES("FAVOURITES"), + @SerializedName("REBLOGS") + REBLOGS("REBLOGS"), + @SerializedName("POLLS") + POLLS("POLLS"), + @SerializedName("TOOTS") + TOOTS("TOOTS"), + @SerializedName("FOLLOWS") + FOLLOWS("FOLLOWS"); + + private final String value; + + NotificationTypeEnum(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonTag.java b/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonTag.java new file mode 100644 index 000000000..65836ef73 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonTag.java @@ -0,0 +1,125 @@ +package app.fedilab.android.ui.fragment.timeline; +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; +import androidx.recyclerview.widget.LinearLayoutManager; + +import java.util.List; + +import app.fedilab.android.BaseMainActivity; +import app.fedilab.android.R; +import app.fedilab.android.client.mastodon.entities.Tag; +import app.fedilab.android.databinding.FragmentPaginationBinding; +import app.fedilab.android.helper.Helper; +import app.fedilab.android.helper.MastodonHelper; +import app.fedilab.android.ui.drawer.TagAdapter; +import app.fedilab.android.viewmodel.mastodon.SearchVM; + + +public class FragmentMastodonTag extends Fragment { + + + private FragmentPaginationBinding binding; + private TagAdapter tagAdapter; + private String search; + + public View onCreateView(@NonNull LayoutInflater inflater, + ViewGroup container, Bundle savedInstanceState) { + if (getArguments() != null) { + search = getArguments().getString(Helper.ARG_SEARCH_KEYWORD, null); + } + + binding = FragmentPaginationBinding.inflate(inflater, container, false); + return binding.getRoot(); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + int c1 = getResources().getColor(R.color.cyanea_accent_reference); + binding.swipeContainer.setProgressBackgroundColorSchemeColor(getResources().getColor(R.color.cyanea_primary_reference)); + binding.swipeContainer.setColorSchemeColors( + c1, c1, c1 + ); + binding.loader.setVisibility(View.VISIBLE); + binding.recyclerView.setVisibility(View.GONE); + router(); + } + + /** + * Router for timelines + */ + private void router() { + if (search != null) { + SearchVM searchVM = new ViewModelProvider(FragmentMastodonTag.this).get(SearchVM.class); + searchVM.search(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, search.trim(), null, "hashtags", false, true, false, 0, null, null, MastodonHelper.STATUSES_PER_CALL) + .observe(getViewLifecycleOwner(), results -> { + if (results != null && results.hashtags != null) { + initializeTagCommonView(results.hashtags); + } + }); + } + } + + public void scrollToTop() { + binding.recyclerView.setAdapter(tagAdapter); + } + + /** + * Intialize the view for tags + * + * @param tags List of {@link Tag} + */ + private void initializeTagCommonView(final List tags) { + binding.loader.setVisibility(View.GONE); + binding.noAction.setVisibility(View.GONE); + binding.swipeContainer.setRefreshing(false); + binding.swipeContainer.setOnRefreshListener(() -> { + binding.swipeContainer.setRefreshing(true); + router(); + }); + if (tags == null || tags.size() == 0) { + binding.recyclerView.setVisibility(View.GONE); + binding.noAction.setVisibility(View.VISIBLE); + binding.noActionText.setText(R.string.no_tags); + return; + } else { + binding.recyclerView.setVisibility(View.VISIBLE); + binding.noAction.setVisibility(View.GONE); + } + tagAdapter = new TagAdapter(tags); + LinearLayoutManager mLayoutManager = new LinearLayoutManager(requireActivity()); + binding.recyclerView.setLayoutManager(mLayoutManager); + binding.recyclerView.setAdapter(tagAdapter); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + binding.recyclerView.setAdapter(null); + tagAdapter = null; + binding = null; + } +} \ No newline at end of file diff --git a/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonTimeline.java b/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonTimeline.java new file mode 100644 index 000000000..9650d7d4e --- /dev/null +++ b/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonTimeline.java @@ -0,0 +1,490 @@ +package app.fedilab.android.ui.fragment.timeline; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + + +import static app.fedilab.android.BaseMainActivity.networkAvailable; + +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.ArrayList; +import java.util.List; + +import app.fedilab.android.BaseMainActivity; +import app.fedilab.android.R; +import app.fedilab.android.client.entities.Timeline; +import app.fedilab.android.client.entities.app.TagTimeline; +import app.fedilab.android.client.mastodon.entities.Account; +import app.fedilab.android.client.mastodon.entities.Marker; +import app.fedilab.android.client.mastodon.entities.Pagination; +import app.fedilab.android.client.mastodon.entities.Status; +import app.fedilab.android.client.mastodon.entities.Statuses; +import app.fedilab.android.databinding.FragmentPaginationBinding; +import app.fedilab.android.helper.Helper; +import app.fedilab.android.helper.MastodonHelper; +import app.fedilab.android.ui.drawer.StatusAdapter; +import app.fedilab.android.viewmodel.mastodon.AccountsVM; +import app.fedilab.android.viewmodel.mastodon.SearchVM; +import app.fedilab.android.viewmodel.mastodon.TimelinesVM; + + +public class FragmentMastodonTimeline extends Fragment { + + + private FragmentPaginationBinding binding; + private TimelinesVM timelinesVM; + private AccountsVM accountsVM; + private boolean flagLoading; + private List statuses; + private String search, searchCache; + private Status statusReport; + private String max_id, min_id; + private StatusAdapter statusAdapter; + private Timeline.TimeLineEnum timelineType; + private List markers; + private String list_id; + private TagTimeline tagTimeline; + private LinearLayoutManager mLayoutManager; + private Account accountTimeline; + private boolean show_replies, show_pinned, media_only, minified; + private String viewModelKey; + + public View onCreateView(@NonNull LayoutInflater inflater, + ViewGroup container, Bundle savedInstanceState) { + + timelineType = Timeline.TimeLineEnum.HOME; + + if (getArguments() != null) { + timelineType = (Timeline.TimeLineEnum) getArguments().get(Helper.ARG_TIMELINE_TYPE); + list_id = getArguments().getString(Helper.ARG_LIST_ID, null); + search = getArguments().getString(Helper.ARG_SEARCH_KEYWORD, null); + searchCache = getArguments().getString(Helper.ARG_SEARCH_KEYWORD_CACHE, null); + tagTimeline = (TagTimeline) getArguments().getSerializable(Helper.ARG_TAG_TIMELINE); + accountTimeline = (Account) getArguments().getSerializable(Helper.ARG_ACCOUNT); + show_replies = getArguments().getBoolean(Helper.ARG_SHOW_REPLIES, false); + show_pinned = getArguments().getBoolean(Helper.ARG_SHOW_PINNED, false); + media_only = getArguments().getBoolean(Helper.ARG_SHOW_MEDIA_ONY, false); + viewModelKey = getArguments().getString(Helper.ARG_VIEW_MODEL_KEY, ""); + minified = getArguments().getBoolean(Helper.ARG_MINIFIED, false); + statusReport = (Status) getArguments().getSerializable(Helper.ARG_STATUS_REPORT); + } + + binding = FragmentPaginationBinding.inflate(inflater, container, false); + return binding.getRoot(); + } + + /** + * Returned list of checked status id for reports + * + * @return List + */ + public List getCheckedStatusesId() { + List stringList = new ArrayList<>(); + for (Status status : statuses) { + if (status.isChecked) { + stringList.add(status.id); + } + } + return stringList; + } + + public void scrollToTop() { + binding.recyclerView.scrollToPosition(0); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + int c1 = getResources().getColor(R.color.cyanea_accent_reference); + binding.swipeContainer.setProgressBackgroundColorSchemeColor(getResources().getColor(R.color.cyanea_primary_reference)); + binding.swipeContainer.setColorSchemeColors( + c1, c1, c1 + ); + + timelinesVM = new ViewModelProvider(FragmentMastodonTimeline.this).get(viewModelKey, TimelinesVM.class); + accountsVM = new ViewModelProvider(FragmentMastodonTimeline.this).get(viewModelKey, AccountsVM.class); + + + binding.loader.setVisibility(View.VISIBLE); + binding.recyclerView.setVisibility(View.GONE); + //Markers for home and notifications to get last read ones + markers = new ArrayList<>(); + max_id = statusReport != null ? statusReport.id : null; + flagLoading = false; + router(null); + } + + /** + * Intialize the common view for statuses on different timelines + * + * @param statuses {@link Statuses} + */ + private void initializeStatusesCommonView(final Statuses statuses) { + if (binding == null) { + return; + } + binding.loader.setVisibility(View.GONE); + binding.noAction.setVisibility(View.GONE); + binding.swipeContainer.setRefreshing(false); + binding.swipeContainer.setOnRefreshListener(() -> { + binding.swipeContainer.setRefreshing(true); + max_id = null; + router(null); + }); + + if (statuses == null || statuses.statuses == null || statuses.statuses.size() == 0) { + binding.noAction.setVisibility(View.VISIBLE); + return; + } + + binding.recyclerView.setVisibility(View.VISIBLE); + if (statusAdapter != null && this.statuses != null) { + int size = this.statuses.size(); + this.statuses.clear(); + this.statuses = new ArrayList<>(); + statusAdapter.notifyItemRangeRemoved(0, size); + } + if (this.statuses == null) { + this.statuses = new ArrayList<>(); + } + if (statusReport != null) { + this.statuses.add(statusReport); + } + this.statuses.addAll(statuses.statuses); + + max_id = this.statuses.get(this.statuses.size() - 1).id; + min_id = this.statuses.get(0).id; + + statusAdapter = new StatusAdapter(this.statuses, timelineType == Timeline.TimeLineEnum.REMOTE, minified); + + if (statusReport != null) { + scrollToTop(); + } + mLayoutManager = new LinearLayoutManager(requireActivity()); + binding.recyclerView.setLayoutManager(mLayoutManager); + binding.recyclerView.setAdapter(statusAdapter); + + + if (searchCache == null) { + binding.recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { + if (requireActivity() instanceof BaseMainActivity) { + if (dy < 0 && !((BaseMainActivity) requireActivity()).getFloatingVisibility()) + ((BaseMainActivity) requireActivity()).manageFloatingButton(true); + if (dy > 0 && ((BaseMainActivity) requireActivity()).getFloatingVisibility()) + ((BaseMainActivity) requireActivity()).manageFloatingButton(false); + } + int firstVisibleItem = mLayoutManager.findFirstVisibleItemPosition(); + if (dy > 0) { + int visibleItemCount = mLayoutManager.getChildCount(); + int totalItemCount = mLayoutManager.getItemCount(); + if (firstVisibleItem + visibleItemCount == totalItemCount) { + if (!flagLoading) { + flagLoading = true; + binding.loadingNextElements.setVisibility(View.VISIBLE); + router(DIRECTION.BOTTOM); + } + } else { + binding.loadingNextElements.setVisibility(View.GONE); + } + } else if (firstVisibleItem == 0) { //Scroll top and item is zero + if (!flagLoading) { + flagLoading = true; + binding.loadingNextElements.setVisibility(View.VISIBLE); + router(DIRECTION.TOP); + } + } + } + }); + } + } + + /** + * Update view and pagination when scrolling down + * + * @param fetched_statuses Statuses + */ + private void dealWithPagination(Statuses fetched_statuses, DIRECTION direction) { + flagLoading = false; + if (binding == null) { + return; + } + binding.loadingNextElements.setVisibility(View.GONE); + if (statuses != null && fetched_statuses != null && fetched_statuses.statuses != null) { + int startId = 0; + //There are some statuses present in the timeline + if (statuses.size() > 0) { + startId = statuses.size(); + } + + if (direction == DIRECTION.TOP) { + statuses.addAll(0, fetched_statuses.statuses); + statusAdapter.notifyItemRangeInserted(0, fetched_statuses.statuses.size()); + //Maybe a better solution but max_id excludes fetched id, so when fetching with min_id we have to scroll top of one status to get it. + if (fetched_statuses.statuses.size() > 0) { + binding.recyclerView.scrollToPosition(fetched_statuses.statuses.size() - 1); + } + } else { + statuses.addAll(fetched_statuses.statuses); + statusAdapter.notifyItemRangeInserted(startId, fetched_statuses.statuses.size()); + } + max_id = statuses.get(statuses.size() - 1).id; + min_id = statuses.get(0).id; + } + } + + @Override + public void onPause() { + super.onPause(); + storeMarker(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + //Update last read id for home timeline + storeMarker(); + if (binding != null) { + binding.recyclerView.setAdapter(null); + } + statusAdapter = null; + binding = null; + } + + private void storeMarker() { + if (timelineType == Timeline.TimeLineEnum.HOME && mLayoutManager != null) { + int position = mLayoutManager.findFirstVisibleItemPosition(); + if (statuses != null && statuses.size() > position) { + try { + Status status = statuses.get(position); + timelinesVM.addMarker(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, status.id, null); + } catch (Exception ignored) { + } + } + } + } + + private void router(DIRECTION direction) { + if (networkAvailable == BaseMainActivity.status.UNKNOWN) { + new Thread(() -> { + if (networkAvailable == BaseMainActivity.status.UNKNOWN) { + networkAvailable = Helper.isConnectedToInternet(requireActivity(), BaseMainActivity.currentInstance); + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + Runnable myRunnable = () -> route(direction); + mainHandler.post(myRunnable); + }).start(); + } else { + route(direction); + } + } + + /** + * Router for timelines + * + * @param direction - DIRECTION null if first call, then is set to TOP or BOTTOM depending of scroll + */ + private void route(DIRECTION direction) { + // --- HOME TIMELINE --- + if (timelineType == Timeline.TimeLineEnum.HOME) { + //for more visibility it's done through loadHomeStrategy method + loadHomeStrategy(direction); + } else if (timelineType == Timeline.TimeLineEnum.LOCAL) { //LOCAL TIMELINE + if (direction == null) { + timelinesVM.getPublic(BaseMainActivity.currentToken, BaseMainActivity.currentInstance, true, false, false, null, null, null, MastodonHelper.statusesPerCall(requireActivity())) + .observe(getViewLifecycleOwner(), this::initializeStatusesCommonView); + } else if (direction == DIRECTION.BOTTOM) { + timelinesVM.getPublic(BaseMainActivity.currentToken, BaseMainActivity.currentInstance, true, false, false, max_id, null, null, MastodonHelper.statusesPerCall(requireActivity())) + .observe(getViewLifecycleOwner(), statusesBottom -> dealWithPagination(statusesBottom, DIRECTION.BOTTOM)); + } else if (direction == DIRECTION.TOP) { + timelinesVM.getPublic(BaseMainActivity.currentToken, BaseMainActivity.currentInstance, true, false, false, null, null, min_id, MastodonHelper.statusesPerCall(requireActivity())) + .observe(getViewLifecycleOwner(), statusesBottom -> dealWithPagination(statusesBottom, DIRECTION.TOP)); + } + } else if (timelineType == Timeline.TimeLineEnum.PUBLIC) { //PUBLIC TIMELINE + if (direction == null) { + timelinesVM.getPublic(BaseMainActivity.currentToken, BaseMainActivity.currentInstance, false, true, false, null, null, null, MastodonHelper.statusesPerCall(requireActivity())) + .observe(getViewLifecycleOwner(), this::initializeStatusesCommonView); + } else if (direction == DIRECTION.BOTTOM) { + timelinesVM.getPublic(BaseMainActivity.currentToken, BaseMainActivity.currentInstance, false, true, false, max_id, null, null, MastodonHelper.statusesPerCall(requireActivity())) + .observe(getViewLifecycleOwner(), statusesBottom -> dealWithPagination(statusesBottom, DIRECTION.BOTTOM)); + } else if (direction == DIRECTION.TOP) { + timelinesVM.getPublic(BaseMainActivity.currentToken, BaseMainActivity.currentInstance, false, true, false, null, null, min_id, MastodonHelper.statusesPerCall(requireActivity())) + .observe(getViewLifecycleOwner(), statusesBottom -> dealWithPagination(statusesBottom, DIRECTION.TOP)); + } + } else if (timelineType == Timeline.TimeLineEnum.LIST) { //LIST TIMELINE + if (direction == null) { + timelinesVM.getList(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, list_id, null, null, null, MastodonHelper.statusesPerCall(requireActivity())) + .observe(getViewLifecycleOwner(), this::initializeStatusesCommonView); + } else if (direction == DIRECTION.BOTTOM) { + timelinesVM.getList(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, list_id, max_id, null, null, MastodonHelper.statusesPerCall(requireActivity())) + .observe(getViewLifecycleOwner(), statusesBottom -> dealWithPagination(statusesBottom, DIRECTION.BOTTOM)); + } else if (direction == DIRECTION.TOP) { + timelinesVM.getList(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, list_id, null, null, min_id, MastodonHelper.statusesPerCall(requireActivity())) + .observe(getViewLifecycleOwner(), statusesBottom -> dealWithPagination(statusesBottom, DIRECTION.TOP)); + } + } else if (timelineType == Timeline.TimeLineEnum.TAG) { //TAG TIMELINE + if (tagTimeline == null) { + tagTimeline = new TagTimeline(); + tagTimeline.name = search; + } + if (direction == null) { + timelinesVM.getHashTag(BaseMainActivity.currentToken, BaseMainActivity.currentInstance, tagTimeline.name, false, tagTimeline.isART, tagTimeline.all, tagTimeline.any, tagTimeline.none, null, null, null, MastodonHelper.statusesPerCall(requireActivity())) + .observe(getViewLifecycleOwner(), this::initializeStatusesCommonView); + } else if (direction == DIRECTION.BOTTOM) { + timelinesVM.getHashTag(BaseMainActivity.currentToken, BaseMainActivity.currentInstance, tagTimeline.name, false, tagTimeline.isART, tagTimeline.all, tagTimeline.any, tagTimeline.none, max_id, null, null, MastodonHelper.statusesPerCall(requireActivity())) + .observe(getViewLifecycleOwner(), statusesBottom -> dealWithPagination(statusesBottom, DIRECTION.BOTTOM)); + } else if (direction == DIRECTION.TOP) { + timelinesVM.getHashTag(BaseMainActivity.currentToken, BaseMainActivity.currentInstance, tagTimeline.name, false, tagTimeline.isART, tagTimeline.all, tagTimeline.any, tagTimeline.none, null, null, min_id, MastodonHelper.statusesPerCall(requireActivity())) + .observe(getViewLifecycleOwner(), statusesBottom -> dealWithPagination(statusesBottom, DIRECTION.TOP)); + } + } else if (timelineType == Timeline.TimeLineEnum.ACCOUNT_TIMELINE) { //PROFILE TIMELINES + if (direction == null) { + if (show_pinned) { + //Fetch pinned statuses to display them at the top + accountsVM.getAccountStatuses(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, accountTimeline.id, null, null, null, null, null, false, true, MastodonHelper.statusesPerCall(requireActivity())) + .observe(getViewLifecycleOwner(), pinnedStatuses -> accountsVM.getAccountStatuses(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, accountTimeline.id, null, null, null, show_replies, !show_replies, false, false, MastodonHelper.statusesPerCall(requireActivity())) + .observe(getViewLifecycleOwner(), otherStatuses -> { + otherStatuses.statuses.addAll(0, pinnedStatuses.statuses); + initializeStatusesCommonView(otherStatuses); + })); + } else { + accountsVM.getAccountStatuses(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, accountTimeline.id, null, null, null, show_replies, !show_replies, media_only, false, MastodonHelper.statusesPerCall(requireActivity())) + .observe(getViewLifecycleOwner(), this::initializeStatusesCommonView); + } + } else if (direction == DIRECTION.BOTTOM) { + accountsVM.getAccountStatuses(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, accountTimeline.id, max_id, null, null, show_replies, !show_replies, media_only, false, MastodonHelper.statusesPerCall(requireActivity())) + .observe(getViewLifecycleOwner(), statusesBottom -> dealWithPagination(statusesBottom, DIRECTION.BOTTOM)); + } else { + flagLoading = false; + } + } else if (search != null) { + SearchVM searchVM = new ViewModelProvider(FragmentMastodonTimeline.this).get(viewModelKey, SearchVM.class); + searchVM.search(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, search.trim(), null, null, false, true, false, 0, null, null, MastodonHelper.STATUSES_PER_CALL) + .observe(getViewLifecycleOwner(), results -> { + Statuses statuses = new Statuses(); + statuses.statuses = results.statuses; + statuses.pagination = new Pagination(); + initializeStatusesCommonView(statuses); + }); + } else if (searchCache != null) { + SearchVM searchVM = new ViewModelProvider(FragmentMastodonTimeline.this).get(viewModelKey, SearchVM.class); + searchVM.searchCache(BaseMainActivity.currentInstance, BaseMainActivity.currentUserID, searchCache.trim()) + .observe(getViewLifecycleOwner(), results -> { + Statuses statuses = new Statuses(); + statuses.statuses = results.statuses; + statuses.pagination = new Pagination(); + initializeStatusesCommonView(statuses); + }); + } else if (timelineType == Timeline.TimeLineEnum.FAVOURITE_TIMELINE) { + if (direction == null) { + accountsVM.getFavourites(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, String.valueOf(MastodonHelper.statusesPerCall(requireActivity())), null, null) + .observe(getViewLifecycleOwner(), this::initializeStatusesCommonView); + } else if (direction == DIRECTION.BOTTOM) { + accountsVM.getFavourites(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, String.valueOf(MastodonHelper.statusesPerCall(requireActivity())), null, max_id) + .observe(getViewLifecycleOwner(), statusesBottom -> dealWithPagination(statusesBottom, DIRECTION.BOTTOM)); + } else if (direction == DIRECTION.TOP) { + accountsVM.getFavourites(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, String.valueOf(MastodonHelper.statusesPerCall(requireActivity())), min_id, null) + .observe(getViewLifecycleOwner(), statusesBottom -> dealWithPagination(statusesBottom, DIRECTION.TOP)); + } + } else if (timelineType == Timeline.TimeLineEnum.BOOKMARK_TIMELINE) { + if (direction == null) { + accountsVM.getBookmarks(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, String.valueOf(MastodonHelper.statusesPerCall(requireActivity())), null, null, null) + .observe(getViewLifecycleOwner(), this::initializeStatusesCommonView); + } else if (direction == DIRECTION.BOTTOM) { + accountsVM.getBookmarks(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, String.valueOf(MastodonHelper.statusesPerCall(requireActivity())), null, max_id, null) + .observe(getViewLifecycleOwner(), statusesBottom -> dealWithPagination(statusesBottom, DIRECTION.BOTTOM)); + } else if (direction == DIRECTION.TOP) { + accountsVM.getBookmarks(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, String.valueOf(MastodonHelper.statusesPerCall(requireActivity())), min_id, null, null) + .observe(getViewLifecycleOwner(), statusesBottom -> dealWithPagination(statusesBottom, DIRECTION.TOP)); + } + } + } + + /** + * Load home timeline strategy + * + * @param direction - DIRECTION enum + */ + private void loadHomeStrategy(DIRECTION direction) { + //When no direction is provided, it means it's the first call + if (direction == null) { + //Two ways, depending of the Internet connection + //Connection is available toots are loaded remotely + if (networkAvailable == BaseMainActivity.status.CONNECTED) { + boolean fetchMarker = false; + if (markers.isEmpty()) { + markers.add("home"); + fetchMarker = true; + } + //We search for marker only once - It should not be fetched again when pull to refresh + if (fetchMarker) { + //Search for last position + timelinesVM.getMarker(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, markers).observe(getViewLifecycleOwner(), marker -> { + if (marker != null) { + Marker.MarkerContent markerContent = marker.home; + max_id = markerContent.last_read_id; + min_id = markerContent.last_read_id; + } + timelinesVM.getHome(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, max_id, null, null, MastodonHelper.statusesPerCall(requireActivity()), false) + .observe(getViewLifecycleOwner(), this::initializeStatusesCommonView); + }); + } else { + timelinesVM.getHome(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, max_id, null, null, MastodonHelper.statusesPerCall(requireActivity()), false) + .observe(getViewLifecycleOwner(), this::initializeStatusesCommonView); + } + + } else { + timelinesVM.getHomeCache(BaseMainActivity.currentInstance, BaseMainActivity.currentUserID, null, null) + .observe(getViewLifecycleOwner(), cachedStatus -> { + if (cachedStatus != null && cachedStatus.statuses != null) { + initializeStatusesCommonView(cachedStatus); + } + }); + + } + } else if (direction == DIRECTION.BOTTOM) { + if (networkAvailable == BaseMainActivity.status.CONNECTED) { + timelinesVM.getHome(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, max_id, null, null, MastodonHelper.statusesPerCall(requireActivity()), false) + .observe(getViewLifecycleOwner(), statusesBottom -> dealWithPagination(statusesBottom, DIRECTION.BOTTOM)); + } else { + timelinesVM.getHomeCache(BaseMainActivity.currentInstance, BaseMainActivity.currentUserID, max_id, null) + .observe(getViewLifecycleOwner(), statusesBottom -> dealWithPagination(statusesBottom, DIRECTION.BOTTOM)); + } + } else if (direction == DIRECTION.TOP) { + if (networkAvailable == BaseMainActivity.status.CONNECTED) { + timelinesVM.getHome(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, null, null, min_id, MastodonHelper.statusesPerCall(requireActivity()), false) + .observe(getViewLifecycleOwner(), statusesTop -> dealWithPagination(statusesTop, DIRECTION.TOP)); + } + } + } + + public enum DIRECTION { + TOP, + BOTTOM + } +} \ No newline at end of file diff --git a/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentNotificationContainer.java b/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentNotificationContainer.java new file mode 100644 index 000000000..6ef14d3c5 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentNotificationContainer.java @@ -0,0 +1,260 @@ +package app.fedilab.android.ui.fragment.timeline; +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import android.annotation.SuppressLint; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.core.content.ContextCompat; +import androidx.core.content.res.ResourcesCompat; +import androidx.core.graphics.drawable.DrawableCompat; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; +import androidx.preference.PreferenceManager; + +import com.google.android.material.button.MaterialButton; +import com.google.android.material.tabs.TabLayout; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +import app.fedilab.android.BaseMainActivity; +import app.fedilab.android.R; +import app.fedilab.android.databinding.FragmentNotificationContainerBinding; +import app.fedilab.android.databinding.PopupNotificationSettingsBinding; +import app.fedilab.android.helper.Helper; +import app.fedilab.android.helper.ThemeHelper; +import app.fedilab.android.ui.pageadapter.FedilabNotificationPageAdapter; +import app.fedilab.android.viewmodel.mastodon.NotificationsVM; +import es.dmoral.toasty.Toasty; + + +public class FragmentNotificationContainer extends Fragment { + + private FragmentNotificationContainerBinding binding; + + public View onCreateView(@NonNull LayoutInflater inflater, + ViewGroup container, Bundle savedInstanceState) { + + binding = FragmentNotificationContainerBinding.inflate(inflater, container, false); + return binding.getRoot(); + } + + + @SuppressLint("ApplySharedPref") + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(requireActivity()); + boolean display_all_notification = sharedpreferences.getBoolean(getString(R.string.SET_DISPLAY_ALL_NOTIFICATIONS_TYPE) + BaseMainActivity.currentUserID + BaseMainActivity.currentInstance, false); + if (!display_all_notification) { + binding.tabLayout.addTab(binding.tabLayout.newTab().setText(getString(R.string.all))); + binding.tabLayout.addTab(binding.tabLayout.newTab().setText(getString(R.string.mention))); + binding.tabLayout.setTabMode(TabLayout.MODE_FIXED); + binding.viewpager.setAdapter(new FedilabNotificationPageAdapter(getChildFragmentManager(), false)); + } else { + binding.tabLayout.setTabMode(TabLayout.MODE_SCROLLABLE); + binding.tabLayout.addTab(binding.tabLayout.newTab().setText(getString(R.string.all))); + binding.tabLayout.addTab(binding.tabLayout.newTab().setIcon(R.drawable.ic_baseline_reply_24)); + binding.tabLayout.addTab(binding.tabLayout.newTab().setIcon(R.drawable.ic_baseline_star_24)); + binding.tabLayout.addTab(binding.tabLayout.newTab().setIcon(R.drawable.ic_repeat)); + binding.tabLayout.addTab(binding.tabLayout.newTab().setIcon(R.drawable.ic_baseline_poll_24)); + binding.tabLayout.addTab(binding.tabLayout.newTab().setIcon(R.drawable.ic_baseline_home_24)); + binding.tabLayout.addTab(binding.tabLayout.newTab().setIcon(R.drawable.ic_baseline_person_add_alt_1_24)); + binding.viewpager.setAdapter(new FedilabNotificationPageAdapter(getChildFragmentManager(), true)); + } + AtomicBoolean changes = new AtomicBoolean(false); + binding.settings.setOnClickListener(v -> { + AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(requireActivity(), Helper.dialogStyle()); + PopupNotificationSettingsBinding dialogView = PopupNotificationSettingsBinding.inflate(getLayoutInflater()); + dialogBuilder.setView(dialogView.getRoot()); + + ThemeHelper.changeButtonColor(requireActivity(), dialogView.displayMentions); + ThemeHelper.changeButtonColor(requireActivity(), dialogView.displayFavourites); + ThemeHelper.changeButtonColor(requireActivity(), dialogView.displayReblogs); + ThemeHelper.changeButtonColor(requireActivity(), dialogView.displayPollResults); + ThemeHelper.changeButtonColor(requireActivity(), dialogView.displayUpdatesFromPeople); + ThemeHelper.changeButtonColor(requireActivity(), dialogView.displayFollows); + DrawableCompat.setTintList(DrawableCompat.wrap(dialogView.displayAllCategories.getThumbDrawable()), ThemeHelper.getSwitchCompatThumbDrawable(requireActivity())); + DrawableCompat.setTintList(DrawableCompat.wrap(dialogView.displayAllCategories.getTrackDrawable()), ThemeHelper.getSwitchCompatTrackDrawable(requireActivity())); + dialogView.clearAllNotif.setOnClickListener(v1 -> { + AlertDialog.Builder db = new AlertDialog.Builder(requireActivity(), Helper.dialogStyle()); + db.setTitle(R.string.delete_notification_ask_all); + db.setMessage(R.string.delete_notification_all_warning); + db.setPositiveButton(R.string.delete_all, (dialog, id) -> { + changes.set(true); + NotificationsVM notificationsVM = new ViewModelProvider(FragmentNotificationContainer.this).get(NotificationsVM.class); + notificationsVM.clearNotification(BaseMainActivity.currentInstance, BaseMainActivity.currentToken) + .observe(getViewLifecycleOwner(), unused -> Toasty.info(requireActivity(), R.string.delete_notification_all, Toasty.LENGTH_LONG).show()); + dialog.dismiss(); + }); + db.setNegativeButton(R.string.cancel, (dialog, id) -> dialog.dismiss()); + AlertDialog alertDialog = db.create(); + alertDialog.show(); + }); + + boolean displayAllCategory = sharedpreferences.getBoolean(getString(R.string.SET_DISPLAY_ALL_NOTIFICATIONS_TYPE) + BaseMainActivity.currentUserID + BaseMainActivity.currentInstance, false); + dialogView.displayAllCategories.setChecked(displayAllCategory); + dialogView.displayAllCategories.setOnCheckedChangeListener((compoundButton, checked) -> { + changes.set(true); + SharedPreferences.Editor editor = sharedpreferences.edit(); + editor.putBoolean(getString(R.string.SET_DISPLAY_ALL_NOTIFICATIONS_TYPE) + BaseMainActivity.currentUserID + BaseMainActivity.currentInstance, checked); + editor.commit(); + }); + dialogView.displayMentions.setChecked(true); + dialogView.displayFavourites.setChecked(true); + dialogView.displayReblogs.setChecked(true); + dialogView.displayPollResults.setChecked(true); + dialogView.displayUpdatesFromPeople.setChecked(true); + dialogView.displayFollows.setChecked(true); + String excludedCategories = sharedpreferences.getString(getString(R.string.SET_EXCLUDED_NOTIFICATIONS_TYPE) + BaseMainActivity.currentUserID + BaseMainActivity.currentInstance, null); + List excludedCategoriesList = new ArrayList<>(); + if (excludedCategories != null) { + String[] categoriesArray = excludedCategories.split("\\|"); + for (String category : categoriesArray) { + switch (category) { + case "mention": + excludedCategoriesList.add("mention"); + dialogView.displayMentions.setChecked(false); + break; + case "favourite": + excludedCategoriesList.add("favourite"); + dialogView.displayFavourites.setChecked(false); + break; + case "reblog": + excludedCategoriesList.add("reblog"); + dialogView.displayReblogs.setChecked(false); + break; + case "poll": + excludedCategoriesList.add("poll"); + dialogView.displayPollResults.setChecked(false); + break; + case "status": + excludedCategoriesList.add("status"); + dialogView.displayUpdatesFromPeople.setChecked(false); + break; + case "follow": + excludedCategoriesList.add("follow"); + dialogView.displayFollows.setChecked(false); + break; + } + } + } + dialogView.displayTypesGroup.addOnButtonCheckedListener((group, checkedId, isChecked) -> { + changes.set(true); + String notificationType = ""; + if (checkedId == R.id.display_mentions) { + notificationType = "mention"; + } else if (checkedId == R.id.display_favourites) { + notificationType = "favourite"; + } else if (checkedId == R.id.display_reblogs) { + notificationType = "reblog"; + } else if (checkedId == R.id.display_poll_results) { + notificationType = "poll"; + } else if (checkedId == R.id.display_updates_from_people) { + notificationType = "status"; + } else if (checkedId == R.id.display_follows) { + notificationType = "follow"; + } + if (isChecked) { + excludedCategoriesList.remove(notificationType); + } else { + if (!excludedCategoriesList.contains(notificationType)) { + excludedCategoriesList.add(notificationType); + } + } + }); + + dialogView.more.setOnClickListener(v1 -> { + if (dialogView.clearAllNotif.getVisibility() == View.VISIBLE) { + dialogView.clearAllNotif.setVisibility(View.GONE); + ((MaterialButton) v1).setIcon(ResourcesCompat.getDrawable(getResources(), R.drawable.ic_baseline_expand_more_24, requireContext().getTheme())); + } else { + dialogView.clearAllNotif.setVisibility(View.VISIBLE); + ((MaterialButton) v1).setIcon(ResourcesCompat.getDrawable(getResources(), R.drawable.ic_baseline_expand_less_24, requireContext().getTheme())); + } + }); + dialogBuilder.setPositiveButton(R.string.close, (dialog, id) -> { + + if (changes.get()) { + SharedPreferences.Editor editor = sharedpreferences.edit(); + if (excludedCategoriesList.size() > 0) { + StringBuilder cat = new StringBuilder(); + for (String category : excludedCategoriesList) { + cat.append(category).append('|'); + } + if (cat.toString().endsWith("|")) { + cat.setLength(cat.length() - 1); + } + editor.putString(getString(R.string.SET_EXCLUDED_NOTIFICATIONS_TYPE) + BaseMainActivity.currentUserID + BaseMainActivity.currentInstance, cat.toString()); + } else { + editor.putString(getString(R.string.SET_EXCLUDED_NOTIFICATIONS_TYPE) + BaseMainActivity.currentUserID + BaseMainActivity.currentInstance, null); + } + + editor.commit(); + ((BaseMainActivity) requireActivity()).refreshFragment(); + } + dialog.dismiss(); + }); + AlertDialog alertDialog = dialogBuilder.create(); + alertDialog.show(); + }); + + binding.tabLayout.setTabTextColors(ThemeHelper.getAttColor(requireActivity(), R.attr.mTextColor), ContextCompat.getColor(requireActivity(), R.color.cyanea_accent_dark_reference)); + binding.tabLayout.setTabIconTint(ThemeHelper.getColorStateList(requireActivity())); + binding.viewpager.addOnPageChangeListener(new TabLayout.TabLayoutOnPageChangeListener(binding.tabLayout)); + binding.tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() { + @Override + public void onTabSelected(TabLayout.Tab tab) { + binding.viewpager.setCurrentItem(tab.getPosition()); + } + + @Override + public void onTabUnselected(TabLayout.Tab tab) { + + } + + @Override + public void onTabReselected(TabLayout.Tab tab) { + Fragment fragment; + if (binding.viewpager.getAdapter() != null) { + fragment = (Fragment) binding.viewpager.getAdapter().instantiateItem(binding.viewpager, tab.getPosition()); + if (fragment instanceof FragmentMastodonNotification) { + FragmentMastodonNotification fragmentMastodonNotification = ((FragmentMastodonNotification) fragment); + fragmentMastodonNotification.scrollToTop(); + } + } + + } + }); + + } + + + @Override + public void onDestroyView() { + super.onDestroyView(); + binding = null; + } + +} diff --git a/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentProfileTimeline.java b/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentProfileTimeline.java new file mode 100644 index 000000000..08428709c --- /dev/null +++ b/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentProfileTimeline.java @@ -0,0 +1,90 @@ +package app.fedilab.android.ui.fragment.timeline; +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; +import androidx.fragment.app.Fragment; + +import com.google.android.material.tabs.TabLayout; + +import app.fedilab.android.R; +import app.fedilab.android.client.mastodon.entities.Account; +import app.fedilab.android.databinding.FragmentProfileTimelinesBinding; +import app.fedilab.android.helper.Helper; +import app.fedilab.android.helper.ThemeHelper; +import app.fedilab.android.ui.pageadapter.FedilabProfilePageAdapter; + +public class FragmentProfileTimeline extends Fragment { + + private Account account; + private FragmentProfileTimelinesBinding binding; + + public View onCreateView(@NonNull LayoutInflater inflater, + ViewGroup container, Bundle savedInstanceState) { + + if (getArguments() != null) { + account = (Account) getArguments().getSerializable(Helper.ARG_ACCOUNT); + } + binding = FragmentProfileTimelinesBinding.inflate(inflater, container, false); + return binding.getRoot(); + } + + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + binding.tabLayout.addTab(binding.tabLayout.newTab().setText(getString(R.string.toots))); + binding.tabLayout.addTab(binding.tabLayout.newTab().setText(getString(R.string.replies))); + binding.tabLayout.addTab(binding.tabLayout.newTab().setText(getString(R.string.media))); + binding.tabLayout.setTabTextColors(ThemeHelper.getAttColor(requireActivity(), R.attr.mTextColor), ContextCompat.getColor(requireActivity(), R.color.cyanea_accent_dark_reference)); + binding.tabLayout.setTabIconTint(ThemeHelper.getColorStateList(requireActivity())); + binding.viewpager.setAdapter(new FedilabProfilePageAdapter( + getChildFragmentManager(), account)); + binding.viewpager.setOffscreenPageLimit(3); + binding.viewpager.addOnPageChangeListener(new TabLayout.TabLayoutOnPageChangeListener(binding.tabLayout)); + binding.tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() { + @Override + public void onTabSelected(TabLayout.Tab tab) { + binding.viewpager.setCurrentItem(tab.getPosition()); + } + + @Override + public void onTabUnselected(TabLayout.Tab tab) { + + } + + @Override + public void onTabReselected(TabLayout.Tab tab) { + + } + }); + + } + + + @Override + public void onDestroyView() { + super.onDestroyView(); + binding = null; + } + +} diff --git a/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentScheduled.java b/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentScheduled.java new file mode 100644 index 000000000..29852b4cd --- /dev/null +++ b/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentScheduled.java @@ -0,0 +1,138 @@ +package app.fedilab.android.ui.fragment.timeline; +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; +import androidx.recyclerview.widget.LinearLayoutManager; + +import java.util.List; + +import app.fedilab.android.BaseMainActivity; +import app.fedilab.android.R; +import app.fedilab.android.client.entities.ScheduledBoost; +import app.fedilab.android.client.entities.StatusDraft; +import app.fedilab.android.client.entities.Timeline; +import app.fedilab.android.databinding.FragmentScheduledBinding; +import app.fedilab.android.exception.DBException; +import app.fedilab.android.helper.Helper; +import app.fedilab.android.helper.MastodonHelper; +import app.fedilab.android.ui.drawer.StatusScheduledAdapter; +import app.fedilab.android.viewmodel.mastodon.StatusesVM; + +public class FragmentScheduled extends Fragment implements StatusScheduledAdapter.ScheduledActions { + + private FragmentScheduledBinding binding; + private Timeline.TimeLineEnum type; + + public View onCreateView(@NonNull LayoutInflater inflater, + ViewGroup container, Bundle savedInstanceState) { + binding = FragmentScheduledBinding.inflate(inflater, container, false); + if (getArguments() != null) { + type = (Timeline.TimeLineEnum) getArguments().getSerializable(Helper.ARG_TIMELINE_TYPE); + } + return binding.getRoot(); + } + + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + binding.loader.setVisibility(View.VISIBLE); + if (type == Timeline.TimeLineEnum.SCHEDULED_TOOT_SERVER) { + StatusesVM statusesVM = new ViewModelProvider(requireActivity()).get(StatusesVM.class); + statusesVM.getScheduledStatuses(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, null, null, null, MastodonHelper.statusesPerCall(requireActivity())) + .observe(requireActivity(), scheduledStatuses -> { + binding.loader.setVisibility(View.GONE); + if (scheduledStatuses != null && scheduledStatuses.scheduledStatuses != null && scheduledStatuses.scheduledStatuses.size() > 0) { + StatusScheduledAdapter statusScheduledAdapter = new StatusScheduledAdapter(scheduledStatuses.scheduledStatuses, null, null); + statusScheduledAdapter.scheduledActions = FragmentScheduled.this; + binding.recyclerView.setAdapter(statusScheduledAdapter); + LinearLayoutManager linearLayoutManager = new LinearLayoutManager(requireActivity()); + binding.recyclerView.setLayoutManager(linearLayoutManager); + + } else { + binding.noAction.setVisibility(View.VISIBLE); + binding.recyclerView.setVisibility(View.GONE); + } + }); + } else if (type == Timeline.TimeLineEnum.SCHEDULED_TOOT_CLIENT) { + new Thread(() -> { + try { + List scheduledDrafts = new StatusDraft(requireActivity()).geStatusDraftScheduledList(BaseMainActivity.accountWeakReference.get()); + Handler mainHandler = new Handler(Looper.getMainLooper()); + binding.loader.setVisibility(View.GONE); + Runnable myRunnable = () -> { + if (scheduledDrafts != null && scheduledDrafts.size() > 0) { + StatusScheduledAdapter statusScheduledAdapter = new StatusScheduledAdapter(null, scheduledDrafts, null); + statusScheduledAdapter.scheduledActions = FragmentScheduled.this; + binding.recyclerView.setAdapter(statusScheduledAdapter); + LinearLayoutManager linearLayoutManager = new LinearLayoutManager(requireActivity()); + binding.recyclerView.setLayoutManager(linearLayoutManager); + + } else { + binding.noAction.setVisibility(View.VISIBLE); + binding.recyclerView.setVisibility(View.GONE); + } + }; + mainHandler.post(myRunnable); + } catch (DBException e) { + e.printStackTrace(); + } + }).start(); + + } else if (type == Timeline.TimeLineEnum.SCHEDULED_BOOST) { + new Thread(() -> { + try { + List scheduledBoosts = new ScheduledBoost(requireActivity()).getScheduled(BaseMainActivity.accountWeakReference.get()); + Handler mainHandler = new Handler(Looper.getMainLooper()); + Runnable myRunnable = () -> { + binding.loader.setVisibility(View.GONE); + if (scheduledBoosts != null && scheduledBoosts.size() > 0) { + StatusScheduledAdapter statusScheduledAdapter = new StatusScheduledAdapter(null, null, scheduledBoosts); + statusScheduledAdapter.scheduledActions = FragmentScheduled.this; + binding.recyclerView.setAdapter(statusScheduledAdapter); + LinearLayoutManager linearLayoutManager = new LinearLayoutManager(requireActivity()); + binding.recyclerView.setLayoutManager(linearLayoutManager); + + } else { + binding.noAction.setVisibility(View.VISIBLE); + binding.noActionText.setText(R.string.no_scheduled_boosts); + binding.recyclerView.setVisibility(View.GONE); + } + }; + mainHandler.post(myRunnable); + } catch (DBException e) { + e.printStackTrace(); + } + }).start(); + } + } + + @Override + public void onAllDeleted() { + binding.noAction.setVisibility(View.VISIBLE); + binding.recyclerView.setVisibility(View.GONE); + } +} diff --git a/app/src/main/java/app/fedilab/android/ui/pageadapter/FedilabNotificationPageAdapter.java b/app/src/main/java/app/fedilab/android/ui/pageadapter/FedilabNotificationPageAdapter.java new file mode 100644 index 000000000..771adeaeb --- /dev/null +++ b/app/src/main/java/app/fedilab/android/ui/pageadapter/FedilabNotificationPageAdapter.java @@ -0,0 +1,100 @@ +package app.fedilab.android.ui.pageadapter; +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + + +import android.os.Bundle; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentStatePagerAdapter; + +import app.fedilab.android.helper.Helper; +import app.fedilab.android.ui.fragment.timeline.FragmentMastodonNotification; + +public class FedilabNotificationPageAdapter extends FragmentStatePagerAdapter { + + private final boolean extended; + private Fragment mCurrentFragment; + + + public FedilabNotificationPageAdapter(FragmentManager fm, boolean extended) { + super(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT); + + this.extended = extended; + } + + public Fragment getCurrentFragment() { + return mCurrentFragment; + } + + @Override + public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) { + if (getCurrentFragment() != object) { + mCurrentFragment = ((Fragment) object); + } + super.setPrimaryItem(container, position, object); + } + + @NonNull + @Override + public Fragment getItem(int position) { + Bundle bundle = new Bundle(); + FragmentMastodonNotification fragmentMastodonNotification = new FragmentMastodonNotification(); + if (!extended) { + switch (position) { + case 0: + bundle.putSerializable(Helper.ARG_NOTIFICATION_TYPE, FragmentMastodonNotification.NotificationTypeEnum.ALL); + break; + case 1: + bundle.putSerializable(Helper.ARG_NOTIFICATION_TYPE, FragmentMastodonNotification.NotificationTypeEnum.MENTIONS); + break; + } + } else { + switch (position) { + case 0: + bundle.putSerializable(Helper.ARG_NOTIFICATION_TYPE, FragmentMastodonNotification.NotificationTypeEnum.ALL); + break; + case 1: + bundle.putSerializable(Helper.ARG_NOTIFICATION_TYPE, FragmentMastodonNotification.NotificationTypeEnum.MENTIONS); + break; + case 2: + bundle.putSerializable(Helper.ARG_NOTIFICATION_TYPE, FragmentMastodonNotification.NotificationTypeEnum.FAVOURITES); + break; + case 3: + bundle.putSerializable(Helper.ARG_NOTIFICATION_TYPE, FragmentMastodonNotification.NotificationTypeEnum.REBLOGS); + break; + case 4: + bundle.putSerializable(Helper.ARG_NOTIFICATION_TYPE, FragmentMastodonNotification.NotificationTypeEnum.POLLS); + break; + case 5: + bundle.putSerializable(Helper.ARG_NOTIFICATION_TYPE, FragmentMastodonNotification.NotificationTypeEnum.TOOTS); + break; + case 6: + bundle.putSerializable(Helper.ARG_NOTIFICATION_TYPE, FragmentMastodonNotification.NotificationTypeEnum.FOLLOWS); + break; + } + } + fragmentMastodonNotification.setArguments(bundle); + return fragmentMastodonNotification; + } + + @Override + public int getCount() { + return extended ? 7 : 2; + } +} diff --git a/app/src/main/java/app/fedilab/android/ui/pageadapter/FedilabPageAdapter.java b/app/src/main/java/app/fedilab/android/ui/pageadapter/FedilabPageAdapter.java new file mode 100644 index 000000000..4622c8066 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/ui/pageadapter/FedilabPageAdapter.java @@ -0,0 +1,109 @@ +package app.fedilab.android.ui.pageadapter; +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import android.os.Bundle; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentStatePagerAdapter; + +import app.fedilab.android.client.entities.Pinned; +import app.fedilab.android.client.entities.Timeline; +import app.fedilab.android.client.entities.app.PinnedTimeline; +import app.fedilab.android.helper.Helper; +import app.fedilab.android.ui.fragment.timeline.FragmentMastodonConversation; +import app.fedilab.android.ui.fragment.timeline.FragmentMastodonTimeline; +import app.fedilab.android.ui.fragment.timeline.FragmentNotificationContainer; + +public class FedilabPageAdapter extends FragmentStatePagerAdapter { + + public static final int BOTTOM_TIMELINE_COUNT = 5; //home, local, public, notification, DM + private final Pinned pinned; + private Fragment mCurrentFragment; + + public FedilabPageAdapter(FragmentManager fm, Pinned pinned) { + super(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT); + this.pinned = pinned; + } + + public Fragment getCurrentFragment() { + return mCurrentFragment; + } + + @Override + public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) { + if (getCurrentFragment() != object) { + mCurrentFragment = ((Fragment) object); + } + super.setPrimaryItem(container, position, object); + } + + @Override + public int getItemPosition(@NonNull Object object) { + // POSITION_NONE makes it possible to reload the PagerAdapter + return POSITION_NONE; + } + + @NonNull + @Override + public Fragment getItem(int position) { + + //Position 3 is for notifications + if (position == 3) { + return new FragmentNotificationContainer(); + } else if (position == 4) { //Position 4 is for DM + return new FragmentMastodonConversation(); + } else { + FragmentMastodonTimeline fragment = new FragmentMastodonTimeline(); + Bundle bundle = new Bundle(); + if (position == 0) { //Home timeline + bundle.putSerializable(Helper.ARG_TIMELINE_TYPE, Timeline.TimeLineEnum.HOME); + } else if (position == 1) { //Local timeline + bundle.putSerializable(Helper.ARG_TIMELINE_TYPE, Timeline.TimeLineEnum.LOCAL); + } else if (position == 2) { //Public timeline + bundle.putSerializable(Helper.ARG_TIMELINE_TYPE, Timeline.TimeLineEnum.PUBLIC); + } else { //Pinned timeline + int pinnedPosition = position - BOTTOM_TIMELINE_COUNT; //Real position has an offset. + PinnedTimeline pinnedTimeline = pinned.pinnedTimelines.get(pinnedPosition); + bundle.putSerializable(Helper.ARG_TIMELINE_TYPE, pinnedTimeline.type); + + if (pinnedTimeline.type == Timeline.TimeLineEnum.LIST) { + bundle.putString(Helper.ARG_LIST_ID, pinnedTimeline.mastodonList.id); + } else if (pinnedTimeline.type == Timeline.TimeLineEnum.TAG) { + String tag = pinnedTimeline.tagTimeline.name.replaceAll("#", ""); + bundle.putString(Helper.ARG_SEARCH_KEYWORD, tag); + } else if (pinnedTimeline.type == Timeline.TimeLineEnum.REMOTE) { + + } + + } + bundle.putString(Helper.ARG_VIEW_MODEL_KEY, "FEDILAB_" + position); + fragment.setArguments(bundle); + return fragment; + } + } + + @Override + public int getCount() { + if (pinned != null && pinned.pinnedTimelines != null) { + return pinned.pinnedTimelines.size() + BOTTOM_TIMELINE_COUNT; + } else { + return BOTTOM_TIMELINE_COUNT; + } + } +} diff --git a/app/src/main/java/app/fedilab/android/ui/pageadapter/FedilabProfilePageAdapter.java b/app/src/main/java/app/fedilab/android/ui/pageadapter/FedilabProfilePageAdapter.java new file mode 100644 index 000000000..f3610552d --- /dev/null +++ b/app/src/main/java/app/fedilab/android/ui/pageadapter/FedilabProfilePageAdapter.java @@ -0,0 +1,88 @@ +package app.fedilab.android.ui.pageadapter; +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import android.os.Bundle; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentStatePagerAdapter; + +import app.fedilab.android.client.entities.Timeline; +import app.fedilab.android.client.mastodon.entities.Account; +import app.fedilab.android.helper.Helper; +import app.fedilab.android.ui.fragment.timeline.FragmentMastodonTimeline; + +public class FedilabProfilePageAdapter extends FragmentStatePagerAdapter { + + private final Account account; + private Fragment mCurrentFragment; + + public FedilabProfilePageAdapter(FragmentManager fm, Account account) { + super(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT); + this.account = account; + } + + public Fragment getCurrentFragment() { + return mCurrentFragment; + } + + @Override + public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) { + if (getCurrentFragment() != object) { + mCurrentFragment = ((Fragment) object); + } + super.setPrimaryItem(container, position, object); + } + + @NonNull + @Override + public Fragment getItem(int position) { + Bundle bundle = new Bundle(); + bundle.putString(Helper.ARG_VIEW_MODEL_KEY, "FEDILAB_" + position); + switch (position) { + case 0: + FragmentMastodonTimeline fragmentProfileTimeline = new FragmentMastodonTimeline(); + bundle.putSerializable(Helper.ARG_TIMELINE_TYPE, Timeline.TimeLineEnum.ACCOUNT_TIMELINE); + bundle.putSerializable(Helper.ARG_ACCOUNT, account); + bundle.putBoolean(Helper.ARG_SHOW_PINNED, true); + fragmentProfileTimeline.setArguments(bundle); + return fragmentProfileTimeline; + case 1: + fragmentProfileTimeline = new FragmentMastodonTimeline(); + bundle.putSerializable(Helper.ARG_TIMELINE_TYPE, Timeline.TimeLineEnum.ACCOUNT_TIMELINE); + bundle.putSerializable(Helper.ARG_ACCOUNT, account); + bundle.putBoolean(Helper.ARG_SHOW_REPLIES, true); + fragmentProfileTimeline.setArguments(bundle); + return fragmentProfileTimeline; + case 2: + fragmentProfileTimeline = new FragmentMastodonTimeline(); + bundle.putSerializable(Helper.ARG_TIMELINE_TYPE, Timeline.TimeLineEnum.ACCOUNT_TIMELINE); + bundle.putSerializable(Helper.ARG_ACCOUNT, account); + bundle.putBoolean(Helper.ARG_SHOW_MEDIA_ONY, true); + fragmentProfileTimeline.setArguments(bundle); + return fragmentProfileTimeline; + default: + return new FragmentMastodonTimeline(); + } + } + + @Override + public int getCount() { + return 3; + } +} diff --git a/app/src/main/java/app/fedilab/android/ui/pageadapter/FedilabProfileTLPageAdapter.java b/app/src/main/java/app/fedilab/android/ui/pageadapter/FedilabProfileTLPageAdapter.java new file mode 100644 index 000000000..c2e32d490 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/ui/pageadapter/FedilabProfileTLPageAdapter.java @@ -0,0 +1,87 @@ +package app.fedilab.android.ui.pageadapter; +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import android.os.Bundle; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentStatePagerAdapter; + +import app.fedilab.android.client.mastodon.entities.Account; +import app.fedilab.android.helper.Helper; +import app.fedilab.android.ui.fragment.timeline.FragmentMastodonAccount; +import app.fedilab.android.ui.fragment.timeline.FragmentMastodonTimeline; +import app.fedilab.android.ui.fragment.timeline.FragmentProfileTimeline; + +public class FedilabProfileTLPageAdapter extends FragmentStatePagerAdapter { + + private final Account account; + private Fragment mCurrentFragment; + + public FedilabProfileTLPageAdapter(FragmentManager fm, Account account) { + super(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT); + this.account = account; + } + + public Fragment getCurrentFragment() { + return mCurrentFragment; + } + + @Override + public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) { + if (getCurrentFragment() != object) { + mCurrentFragment = ((Fragment) object); + } + super.setPrimaryItem(container, position, object); + } + + @NonNull + @Override + public Fragment getItem(int position) { + switch (position) { + + case 0: + FragmentProfileTimeline fragmentProfileTimeline = new FragmentProfileTimeline(); + Bundle bundle = new Bundle(); + bundle.putSerializable(Helper.ARG_ACCOUNT, account); + fragmentProfileTimeline.setArguments(bundle); + return fragmentProfileTimeline; + case 1: + case 2: + FragmentMastodonAccount fragmentMastodonAccount = new FragmentMastodonAccount(); + bundle = new Bundle(); + bundle.putSerializable(Helper.ARG_ACCOUNT, account); + bundle.putString(Helper.ARG_VIEW_MODEL_KEY, "FEDILAB_" + position); + bundle.putSerializable(Helper.ARG_FOLLOW_TYPE, position == 1 ? follow_type.FOLLOWING : follow_type.FOLLOWERS); + fragmentMastodonAccount.setArguments(bundle); + return fragmentMastodonAccount; + default: + return new FragmentMastodonTimeline(); + } + } + + @Override + public int getCount() { + return 3; + } + + public enum follow_type { + FOLLOWING, + FOLLOWERS + } +} diff --git a/app/src/main/java/app/fedilab/android/ui/pageadapter/FedilabScheduledPageAdapter.java b/app/src/main/java/app/fedilab/android/ui/pageadapter/FedilabScheduledPageAdapter.java new file mode 100644 index 000000000..df47eba16 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/ui/pageadapter/FedilabScheduledPageAdapter.java @@ -0,0 +1,73 @@ +package app.fedilab.android.ui.pageadapter; +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import android.os.Bundle; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentStatePagerAdapter; + +import app.fedilab.android.client.entities.Timeline; +import app.fedilab.android.helper.Helper; +import app.fedilab.android.ui.fragment.timeline.FragmentScheduled; + +public class FedilabScheduledPageAdapter extends FragmentStatePagerAdapter { + + private Fragment mCurrentFragment; + + public FedilabScheduledPageAdapter(FragmentManager fm) { + super(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT); + } + + public Fragment getCurrentFragment() { + return mCurrentFragment; + } + + @Override + public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) { + if (getCurrentFragment() != object) { + mCurrentFragment = ((Fragment) object); + } + super.setPrimaryItem(container, position, object); + } + + @NonNull + @Override + public Fragment getItem(int position) { + Bundle bundle = new Bundle(); + bundle.putString(Helper.ARG_VIEW_MODEL_KEY, "FEDILAB_" + position); + FragmentScheduled fragmentScheduled = new FragmentScheduled(); + switch (position) { + case 1: + bundle.putSerializable(Helper.ARG_TIMELINE_TYPE, Timeline.TimeLineEnum.SCHEDULED_TOOT_CLIENT); + break; + case 2: + bundle.putSerializable(Helper.ARG_TIMELINE_TYPE, Timeline.TimeLineEnum.SCHEDULED_BOOST); + break; + default: + bundle.putSerializable(Helper.ARG_TIMELINE_TYPE, Timeline.TimeLineEnum.SCHEDULED_TOOT_SERVER); + } + fragmentScheduled.setArguments(bundle); + return fragmentScheduled; + } + + @Override + public int getCount() { + return 3; + } +} diff --git a/app/src/main/java/app/fedilab/android/viewmodel/mastodon/AccountsVM.java b/app/src/main/java/app/fedilab/android/viewmodel/mastodon/AccountsVM.java new file mode 100644 index 000000000..8b8236d84 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/viewmodel/mastodon/AccountsVM.java @@ -0,0 +1,1593 @@ +package app.fedilab.android.viewmodel.mastodon; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import android.app.Application; +import android.net.Uri; +import android.os.Handler; +import android.os.Looper; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import app.fedilab.android.BaseMainActivity; +import app.fedilab.android.R; +import app.fedilab.android.client.mastodon.MastodonAccountsService; +import app.fedilab.android.client.mastodon.entities.Account; +import app.fedilab.android.client.mastodon.entities.Accounts; +import app.fedilab.android.client.mastodon.entities.FeaturedTag; +import app.fedilab.android.client.mastodon.entities.Field; +import app.fedilab.android.client.mastodon.entities.Filter; +import app.fedilab.android.client.mastodon.entities.IdentityProof; +import app.fedilab.android.client.mastodon.entities.MastodonList; +import app.fedilab.android.client.mastodon.entities.Pagination; +import app.fedilab.android.client.mastodon.entities.Preferences; +import app.fedilab.android.client.mastodon.entities.RelationShip; +import app.fedilab.android.client.mastodon.entities.Report; +import app.fedilab.android.client.mastodon.entities.Source; +import app.fedilab.android.client.mastodon.entities.Status; +import app.fedilab.android.client.mastodon.entities.Statuses; +import app.fedilab.android.client.mastodon.entities.Tag; +import app.fedilab.android.client.mastodon.entities.Token; +import app.fedilab.android.helper.Helper; +import app.fedilab.android.helper.MastodonHelper; +import app.fedilab.android.helper.SpannableHelper; +import okhttp3.MultipartBody; +import okhttp3.OkHttpClient; +import retrofit2.Call; +import retrofit2.Response; +import retrofit2.Retrofit; +import retrofit2.converter.gson.GsonConverterFactory; + +public class AccountsVM extends AndroidViewModel { + + + final OkHttpClient okHttpClient = new OkHttpClient.Builder() + .readTimeout(60, TimeUnit.SECONDS) + .connectTimeout(60, TimeUnit.SECONDS) + .proxy(Helper.getProxy(getApplication().getApplicationContext())) + .build(); + + + private MutableLiveData accountMutableLiveData; + private MutableLiveData> accountListMutableLiveData; + private MutableLiveData statusesMutableLiveData; + private MutableLiveData accountsMutableLiveData; + private MutableLiveData> statusListMutableLiveData; + private MutableLiveData featuredTagMutableLiveData; + private MutableLiveData> featuredTagListMutableLiveData; + private MutableLiveData> mastodonListListMutableLiveData; + private MutableLiveData> identityProofListMutableLiveData; + private MutableLiveData relationShipMutableLiveData; + private MutableLiveData> relationShipListMutableLiveData; + private MutableLiveData filterMutableLiveData; + private MutableLiveData> filterListMutableLiveData; + private MutableLiveData> tagListMutableLiveData; + private MutableLiveData preferencesMutableLiveData; + private MutableLiveData tokenMutableLiveData; + private MutableLiveData> stringListMutableLiveData; + private MutableLiveData reportMutableLiveData; + + public AccountsVM(@NonNull Application application) { + super(application); + } + + private MastodonAccountsService init(String instance) { + Retrofit retrofit = new Retrofit.Builder() + .baseUrl("https://" + instance + "/api/v1/") + .addConverterFactory(GsonConverterFactory.create()) + .client(okHttpClient) + .build(); + return retrofit.create(MastodonAccountsService.class); + } + + /** + * Get connected account + * + * @return LiveData + */ + public LiveData getConnectedAccount(@NonNull String instance, String token) { + MastodonAccountsService mastodonAccountsService = init(instance); + accountMutableLiveData = new MutableLiveData<>(); + new Thread(() -> { + Account account = null; + Call accountCall = mastodonAccountsService.verify_credentials(token); + if (accountCall != null) { + try { + Response accountResponse = accountCall.execute(); + if (accountResponse.isSuccessful()) { + account = accountResponse.body(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + Account finalAccount = account; + Runnable myRunnable = () -> accountMutableLiveData.setValue(finalAccount); + mainHandler.post(myRunnable); + }).start(); + return accountMutableLiveData; + } + + /** + * Register an Account + * + * @param username String + * @param email String + * @param password String + * @param agreement boolean + * @param locale boolean + * @param reason String + * @return Token - {@link Token} + */ + public LiveData registerAccount(@NonNull String instance, String token, + @NonNull String username, + @NonNull String email, + @NonNull String password, + boolean agreement, + @NonNull String locale, + String reason) { + tokenMutableLiveData = new MutableLiveData<>(); + new Thread(() -> { + MastodonAccountsService mastodonAccountsService = init(instance); + Call stringCall = mastodonAccountsService.registerAccount(token, username, email, password, agreement, locale, reason); + Token returnedToken = null; + String errorMessage = null; + if (stringCall != null) { + try { + Response stringResponse = stringCall.execute(); + if (stringResponse.isSuccessful()) { + returnedToken = stringResponse.body(); + } else { + if (stringResponse.errorBody() != null) { + errorMessage = stringResponse.errorBody().string(); + } + } + } catch (IOException e) { + e.printStackTrace(); + errorMessage = e.getMessage() != null ? e.getMessage() : getApplication().getString(R.string.toast_error); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + Token finalReturnedToken = returnedToken; + String finalErrorMessage = errorMessage; + Runnable myRunnable = () -> { + if (finalErrorMessage != null) { + Helper.sendToastMessage(getApplication(), Helper.RECEIVE_TOAST_TYPE_ERROR, finalErrorMessage); + } + tokenMutableLiveData.setValue(finalReturnedToken); + }; + mainHandler.post(myRunnable); + }).start(); + return tokenMutableLiveData; + } + + /** + * Allow to only upload avatar or header when editing profile + * + * @return {@link LiveData} containing an {@link Account} + */ + public LiveData updateProfilePicture(@NonNull String instance, String token, Uri uri, UpdateMediaType type) { + MastodonAccountsService mastodonAccountsService = init(instance); + accountMutableLiveData = new MutableLiveData<>(); + new Thread(() -> { + Account account = null; + MultipartBody.Part avatarMultipartBody = null; + MultipartBody.Part headerMultipartBody = null; + + if (type == UpdateMediaType.AVATAR) { + avatarMultipartBody = Helper.getMultipartBody(getApplication(), "avatar", uri); + } else if (type == UpdateMediaType.HEADER) { + headerMultipartBody = Helper.getMultipartBody(getApplication(), "header", uri); + } + Call accountCall = mastodonAccountsService.update_media( + token, avatarMultipartBody, + headerMultipartBody); + if (accountCall != null) { + try { + Response accountResponse = accountCall.execute(); + if (accountResponse.isSuccessful()) { + account = accountResponse.body(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + Account finalAccount = account; + Runnable myRunnable = () -> accountMutableLiveData.setValue(finalAccount); + mainHandler.post(myRunnable); + }).start(); + return accountMutableLiveData; + } + + /** + * Update account credentials + * + * @param discoverable Whether the account should be shown in the profile directory. + * @param bot Whether the account has a bot flag. + * @param displayName The display name to use for the profile. + * @param note The account bio. + * @param locked Whether manual approval of follow requests is required. + * @param privacy Default post privacy for authored statuses. + * @param sensitive Whether to mark authored statuses as sensitive by default. + * @param language Default language to use for authored statuses. (ISO 6391) + * @param fields Profile metadata name (By default, max 4 fields and 255 characters per property/value) + * @return {@link LiveData} containing an {@link Account} + */ + public LiveData updateCredentials(@NonNull String instance, String token, + Boolean discoverable, + Boolean bot, + String displayName, + String note, + Boolean locked, + String privacy, + Boolean sensitive, + String language, + LinkedHashMap fields + ) { + MastodonAccountsService mastodonAccountsService = init(instance); + accountMutableLiveData = new MutableLiveData<>(); + new Thread(() -> { + Account account = null; + Account.AccountParams accountParams = new Account.AccountParams(); + accountParams.bot = bot; + accountParams.discoverable = discoverable; + accountParams.display_name = displayName; + accountParams.note = note; + accountParams.locked = locked; + accountParams.source = new Source.SourceParams(); + accountParams.source.privacy = privacy; + accountParams.source.language = language; + accountParams.source.sensitive = sensitive; + accountParams.fields = fields; + Call accountCall = mastodonAccountsService.update_credentials(token, accountParams); + // Call accountCall = mastodonAccountsService.update_credentials(token, discoverable, bot, displayName, note, locked, privacy, sensitive, language, fields); + if (accountCall != null) { + try { + Response accountResponse = accountCall.execute(); + if (accountResponse.isSuccessful()) { + account = accountResponse.body(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + Account finalAccount = account; + Runnable myRunnable = () -> accountMutableLiveData.setValue(finalAccount); + mainHandler.post(myRunnable); + }).start(); + return accountMutableLiveData; + } + + /** + * @param id The id of the account + * @return {@link LiveData} containing an {@link Account} + */ + public LiveData getAccount(@NonNull String instance, String token, @NonNull String id) { + accountMutableLiveData = new MutableLiveData<>(); + MastodonAccountsService mastodonAccountsService = init(instance); + new Thread(() -> { + Account account = null; + Call accountCall = mastodonAccountsService.getAccount(token, id); + if (accountCall != null) { + try { + Response accountResponse = accountCall.execute(); + if (accountResponse.isSuccessful()) { + account = accountResponse.body(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Account finalAccount = account; + if (finalAccount != null) { + SpannableHelper.convertAccount(getApplication().getApplicationContext(), finalAccount); + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + Runnable myRunnable = () -> accountMutableLiveData.setValue(finalAccount); + mainHandler.post(myRunnable); + }).start(); + return accountMutableLiveData; + } + + /** + * Statuses posted to the given account. + * + * @param id The id of the account + * @return {@link LiveData} containing a {@link Statuses}. Note: Not to be confused with {@link Status} + */ + public LiveData getAccountStatuses(@NonNull String instance, String token, @NonNull String id, + String maxId, + String sinceId, + String minId, + Boolean excludeReplies, + Boolean excludeReblogs, + Boolean only_media, + Boolean pinned, + int count) { + statusesMutableLiveData = new MutableLiveData<>(); + MastodonAccountsService mastodonAccountsService = init(instance); + new Thread(() -> { + List statusList = null; + Pagination pagination = null; + Call> accountStatusesCall = mastodonAccountsService.getAccountStatuses( + token, id, maxId, sinceId, minId, excludeReplies, excludeReblogs, only_media, pinned, count); + if (accountStatusesCall != null) { + try { + Response> accountStatusesResponse = accountStatusesCall.execute(); + if (accountStatusesResponse.isSuccessful()) { + statusList = SpannableHelper.convertStatus(getApplication().getApplicationContext(), accountStatusesResponse.body()); + pagination = MastodonHelper.getPaginationStatus(statusList); + + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Statuses statuses = new Statuses(); + statuses.statuses = statusList; + statuses.pagination = pagination; + Handler mainHandler = new Handler(Looper.getMainLooper()); + Runnable myRunnable = () -> statusesMutableLiveData.setValue(statuses); + mainHandler.post(myRunnable); + }).start(); + return statusesMutableLiveData; + } + + /** + * Accounts which follow the given account, if network is not hidden by the account owner. + * + * @return {@link LiveData} containing an {@link Accounts}. Note: Not to be confused with {@link Account} + */ + public LiveData getAccountFollowers(@NonNull String instance, String token, @NonNull String id, + String maxId, + String sinceId) { + accountsMutableLiveData = new MutableLiveData<>(); + MastodonAccountsService mastodonAccountsService = init(instance); + new Thread(() -> { + List accountList = null; + Pagination pagination = null; + Call> followersCall = mastodonAccountsService.getAccountFollowers(token, id, maxId, sinceId); + if (followersCall != null) { + try { + Response> followersResponse = followersCall.execute(); + if (followersResponse.isSuccessful()) { + accountList = followersResponse.body(); + pagination = MastodonHelper.getPaginationAccount(accountList); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Accounts accounts = new Accounts(); + accounts.accounts = accountList; + accounts.pagination = pagination; + Handler mainHandler = new Handler(Looper.getMainLooper()); + Runnable myRunnable = () -> accountsMutableLiveData.setValue(accounts); + mainHandler.post(myRunnable); + }).start(); + return accountsMutableLiveData; + } + + /** + * Accounts which the given account is following, if network is not hidden by the account owner. + * + * @param id The id of the account + * @return {@link LiveData} containing an {@link Accounts}. Note: Not to be confused with {@link Account} + */ + public LiveData getAccountFollowing(@NonNull String instance, String token, @NonNull String id, + String maxId, + String sinceId) { + accountsMutableLiveData = new MutableLiveData<>(); + MastodonAccountsService mastodonAccountsService = init(instance); + new Thread(() -> { + List accountList = null; + Pagination pagination = null; + Call> followingCall = mastodonAccountsService.getAccountFollowing(token, id, maxId, sinceId); + if (followingCall != null) { + try { + Response> followingResponse = followingCall.execute(); + if (followingResponse.isSuccessful()) { + accountList = followingResponse.body(); + pagination = MastodonHelper.getPaginationAccount(accountList); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Accounts accounts = new Accounts(); + accounts.accounts = accountList; + accounts.pagination = pagination; + Handler mainHandler = new Handler(Looper.getMainLooper()); + Runnable myRunnable = () -> accountsMutableLiveData.setValue(accounts); + mainHandler.post(myRunnable); + }).start(); + return accountsMutableLiveData; + } + + /** + * Tags featured by this account. + * + * @param id The id of the account + * @return {@link LiveData} containing a {@link List} of {@link FeaturedTag}s + */ + public LiveData> getAccountFeaturedTags(@NonNull String instance, String token, @NonNull String id) { + featuredTagListMutableLiveData = new MutableLiveData<>(); + MastodonAccountsService mastodonAccountsService = init(instance); + new Thread(() -> { + List featuredTagList = null; + Call> featuredTagsCall = mastodonAccountsService.getAccountFeaturedTags(token, id); + if (featuredTagsCall != null) { + try { + Response> featuredTagsResponse = featuredTagsCall.execute(); + if (featuredTagsResponse.isSuccessful()) { + featuredTagList = featuredTagsResponse.body(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + List finalFeaturedTagList = featuredTagList; + Runnable myRunnable = () -> featuredTagListMutableLiveData.setValue(finalFeaturedTagList); + mainHandler.post(myRunnable); + }).start(); + return featuredTagListMutableLiveData; + } + + /** + * User lists that you have added this account to. + * + * @param id The id of the account + * @return {@link LiveData} containing a {@link List} of {@link MastodonList}s + */ + public LiveData> getListContainingAccount(@NonNull String instance, String token, @NonNull String id) { + mastodonListListMutableLiveData = new MutableLiveData<>(); + MastodonAccountsService mastodonAccountsService = init(instance); + new Thread(() -> { + List mastodonListList = null; + Call> listsCall = mastodonAccountsService.getListContainingAccount(token, id); + if (listsCall != null) { + try { + Response> listsResponse = listsCall.execute(); + if (listsResponse.isSuccessful()) { + mastodonListList = listsResponse.body(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + List finalMastodonListList = mastodonListList; + Runnable myRunnable = () -> mastodonListListMutableLiveData.setValue(finalMastodonListList); + mainHandler.post(myRunnable); + }).start(); + return mastodonListListMutableLiveData; + } + + /** + * List of IdentityProofs + * + * @param id The id of the account + * @return {@link LiveData} containing a {@link List} of {@link IdentityProof}s of the given account + */ + public LiveData> getIdentityProofs(@NonNull String instance, String token, @NonNull String id) { + identityProofListMutableLiveData = new MutableLiveData<>(); + MastodonAccountsService mastodonAccountsService = init(instance); + new Thread(() -> { + List identityProofList = null; + Call> identityProofsCall = mastodonAccountsService.getIdentityProofs(token, id); + if (identityProofsCall != null) { + try { + Response> identityProofsResponse = identityProofsCall.execute(); + if (identityProofsResponse.isSuccessful()) { + identityProofList = identityProofsResponse.body(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + List finalIdentityProofList = identityProofList; + Runnable myRunnable = () -> identityProofListMutableLiveData.setValue(finalIdentityProofList); + mainHandler.post(myRunnable); + }).start(); + return identityProofListMutableLiveData; + } + + /** + * Update account notes + * + * @param id The id of the account + * @param commment note for the account + * @return {@link LiveData} containing the {@link RelationShip} to the given account + */ + public LiveData updateNote(@NonNull String instance, String token, @NonNull String id, String commment) { + relationShipMutableLiveData = new MutableLiveData<>(); + MastodonAccountsService mastodonAccountsService = init(instance); + new Thread(() -> { + RelationShip relationShip = null; + Call noteCall = mastodonAccountsService.note(token, id, commment); + if (noteCall != null) { + try { + Response followResponse = noteCall.execute(); + if (followResponse.isSuccessful()) { + relationShip = followResponse.body(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + RelationShip finalRelationShip = relationShip; + Runnable myRunnable = () -> relationShipMutableLiveData.setValue(finalRelationShip); + mainHandler.post(myRunnable); + }).start(); + return relationShipMutableLiveData; + } + + /** + * Follow the given account. Can also be used to update whether to show reblogs or enable notifications. + * + * @param id The id of the account + * @param reblogs Receive this account's reblogs in home timeline? Defaults to true. + * @param notify Receive notifications when this account posts a status? Defaults to false. + * @return {@link LiveData} containing the {@link RelationShip} to the given account + */ + public LiveData follow(@NonNull String instance, String token, @NonNull String id, boolean reblogs, boolean notify) { + relationShipMutableLiveData = new MutableLiveData<>(); + MastodonAccountsService mastodonAccountsService = init(instance); + new Thread(() -> { + RelationShip relationShip = null; + Call followCall = mastodonAccountsService.follow(token, id, reblogs, notify); + if (followCall != null) { + try { + Response followResponse = followCall.execute(); + if (followResponse.isSuccessful()) { + relationShip = followResponse.body(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + RelationShip finalRelationShip = relationShip; + Runnable myRunnable = () -> relationShipMutableLiveData.setValue(finalRelationShip); + mainHandler.post(myRunnable); + }).start(); + return relationShipMutableLiveData; + } + + /** + * Unfollow the given account. + * + * @param id The id of the account + * @return {@link LiveData} containing the {@link RelationShip} to the given account + */ + public LiveData unfollow(@NonNull String instance, String token, @NonNull String id) { + relationShipMutableLiveData = new MutableLiveData<>(); + MastodonAccountsService mastodonAccountsService = init(instance); + new Thread(() -> { + RelationShip relationShip = null; + Call unfollowCall = mastodonAccountsService.unfollow(token, id); + if (unfollowCall != null) { + try { + Response unfollowResponse = unfollowCall.execute(); + if (unfollowResponse.isSuccessful()) { + relationShip = unfollowResponse.body(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + RelationShip finalRelationShip = relationShip; + Runnable myRunnable = () -> relationShipMutableLiveData.setValue(finalRelationShip); + mainHandler.post(myRunnable); + }).start(); + return relationShipMutableLiveData; + } + + /** + * Block the given account. Clients should filter statuses from this account if received (e.g. due to a boost in the Home timeline) + * + * @param id The id of the account + * @return {@link LiveData} containing the {@link RelationShip} to the given account + */ + public LiveData block(@NonNull String instance, String token, @NonNull String id) { + relationShipMutableLiveData = new MutableLiveData<>(); + MastodonAccountsService mastodonAccountsService = init(instance); + new Thread(() -> { + RelationShip relationShip = null; + Call blockCall = mastodonAccountsService.block(token, id); + if (blockCall != null) { + try { + Response blockResponse = blockCall.execute(); + if (blockResponse.isSuccessful()) { + relationShip = blockResponse.body(); + } + } catch (IOException e) { + e.printStackTrace(); + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + RelationShip finalRelationShip = relationShip; + Runnable myRunnable = () -> relationShipMutableLiveData.setValue(finalRelationShip); + mainHandler.post(myRunnable); + } + }).start(); + return relationShipMutableLiveData; + } + + /** + * Unblock the given account. + * + * @param id The id of the account + * @return {@link LiveData} containing the {@link RelationShip} to the given account + */ + public LiveData unblock(@NonNull String instance, String token, @NonNull String id) { + relationShipMutableLiveData = new MutableLiveData<>(); + MastodonAccountsService mastodonAccountsService = init(instance); + new Thread(() -> { + RelationShip relationShip = null; + Call unblockCall = mastodonAccountsService.unblock(token, id); + if (unblockCall != null) { + try { + Response unblockResponse = unblockCall.execute(); + if (unblockResponse.isSuccessful()) { + relationShip = unblockResponse.body(); + } + } catch (IOException e) { + e.printStackTrace(); + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + RelationShip finalRelationShip = relationShip; + Runnable myRunnable = () -> relationShipMutableLiveData.setValue(finalRelationShip); + mainHandler.post(myRunnable); + } + }).start(); + return relationShipMutableLiveData; + } + + /** + * Mute the given account. Clients should filter statuses and notifications from this account, if received (e.g. due to a boost in the Home timeline). + * + * @param id The id of the account + * @param notifications Mute notifications in addition to statuses? Defaults to true. + * @param duration How long the mute should last, in seconds. Defaults to 0 (indefinite). + * @return {@link LiveData} containing the {@link RelationShip} to the given account + */ + public LiveData mute(@NonNull String instance, String token, @NonNull String id, Boolean notifications, Integer duration) { + relationShipMutableLiveData = new MutableLiveData<>(); + MastodonAccountsService mastodonAccountsService = init(instance); + new Thread(() -> { + RelationShip relationShip = null; + Call muteCall = mastodonAccountsService.mute(token, id, notifications, duration); + if (muteCall != null) { + try { + Response muteResponse = muteCall.execute(); + if (muteResponse.isSuccessful()) { + relationShip = muteResponse.body(); + } + } catch (IOException e) { + e.printStackTrace(); + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + RelationShip finalRelationShip = relationShip; + Runnable myRunnable = () -> relationShipMutableLiveData.setValue(finalRelationShip); + mainHandler.post(myRunnable); + } + }).start(); + return relationShipMutableLiveData; + } + + /** + * Unmute the given account. + * + * @param id The id of the account + * @return {@link LiveData} containing the {@link RelationShip} to the given account + */ + public LiveData unmute(@NonNull String instance, String token, @NonNull String id) { + relationShipMutableLiveData = new MutableLiveData<>(); + MastodonAccountsService mastodonAccountsService = init(instance); + new Thread(() -> { + RelationShip relationShip = null; + Call unmuteCall = mastodonAccountsService.unmute(token, id); + if (unmuteCall != null) { + try { + Response unmuteResponse = unmuteCall.execute(); + if (unmuteResponse.isSuccessful()) { + relationShip = unmuteResponse.body(); + } + } catch (IOException e) { + e.printStackTrace(); + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + RelationShip finalRelationShip = relationShip; + Runnable myRunnable = () -> relationShipMutableLiveData.setValue(finalRelationShip); + mainHandler.post(myRunnable); + } + }).start(); + return relationShipMutableLiveData; + } + + /** + * Add the given account to the user's featured profiles. (Featured profiles are currently shown on the user's own public profile.) + * + * @param id The id of the account + * @return {@link LiveData} containing the {@link RelationShip} to the given account + */ + public LiveData endorse(@NonNull String instance, String token, @NonNull String id) { + relationShipMutableLiveData = new MutableLiveData<>(); + MastodonAccountsService mastodonAccountsService = init(instance); + new Thread(() -> { + RelationShip relationShip = null; + Call endorseCall = mastodonAccountsService.endorse(token, id); + if (endorseCall != null) { + try { + Response endorseResponse = endorseCall.execute(); + if (endorseResponse.isSuccessful()) { + relationShip = endorseResponse.body(); + } + } catch (IOException e) { + e.printStackTrace(); + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + RelationShip finalRelationShip = relationShip; + Runnable myRunnable = () -> relationShipMutableLiveData.setValue(finalRelationShip); + mainHandler.post(myRunnable); + } + }).start(); + return relationShipMutableLiveData; + } + + /** + * Remove the given account from the user's featured profiles. + * + * @param id The id of the account + * @return {@link LiveData} containing the {@link RelationShip} to the given account + */ + public LiveData unendorse(@NonNull String instance, String token, @NonNull String id) { + relationShipMutableLiveData = new MutableLiveData<>(); + new Thread(() -> { + RelationShip relationShip = null; + MastodonAccountsService mastodonAccountsService = init(instance); + Call unendorseCall = mastodonAccountsService.unendorse(token, id); + if (unendorseCall != null) { + try { + Response unendorseResponse = unendorseCall.execute(); + if (unendorseResponse.isSuccessful()) { + relationShip = unendorseResponse.body(); + } + } catch (IOException e) { + e.printStackTrace(); + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + RelationShip finalRelationShip = relationShip; + Runnable myRunnable = () -> relationShipMutableLiveData.setValue(finalRelationShip); + mainHandler.post(myRunnable); + } + }).start(); + return relationShipMutableLiveData; + } + + /** + * Sets a private note on a user. + * + * @param id The id of the account + * @param comment The comment to be set on that user. Provide an empty string or leave out this parameter to clear the currently set note. + * @return {@link LiveData} containing the {@link RelationShip} to the given account + */ + public LiveData note(@NonNull String instance, String token, @NonNull String id, String comment) { + relationShipMutableLiveData = new MutableLiveData<>(); + MastodonAccountsService mastodonAccountsService = init(instance); + new Thread(() -> { + RelationShip relationShip = null; + Call noteCall = mastodonAccountsService.note(token, id, comment); + if (noteCall != null) { + try { + Response noteResponse = noteCall.execute(); + if (noteResponse.isSuccessful()) { + relationShip = noteResponse.body(); + } + } catch (IOException e) { + e.printStackTrace(); + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + RelationShip finalRelationShip = relationShip; + Runnable myRunnable = () -> relationShipMutableLiveData.setValue(finalRelationShip); + mainHandler.post(myRunnable); + } + }).start(); + return relationShipMutableLiveData; + } + + /** + * Find out whether a given account is followed, blocked, muted, etc. + * + * @param ids {@link List} of account IDs to check + * @return {@link LiveData} containing a {@link List} of {@link RelationShip}s to given account(s) + */ + public LiveData> getRelationships(@NonNull String instance, String token, @NonNull List ids) { + relationShipListMutableLiveData = new MutableLiveData<>(); + MastodonAccountsService mastodonAccountsService = init(instance); + new Thread(() -> { + List relationShipList = null; + Call> relationshipsCall = mastodonAccountsService.getRelationships(token, ids); + if (relationshipsCall != null) { + try { + Response> relationshipsResponse = relationshipsCall.execute(); + if (relationshipsResponse.isSuccessful()) { + relationShipList = relationshipsResponse.body(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + List finalRelationShipList = relationShipList; + Runnable myRunnable = () -> relationShipListMutableLiveData.setValue(finalRelationShipList); + mainHandler.post(myRunnable); + }).start(); + return relationShipListMutableLiveData; + } + + /** + * Search for matching accounts by username or display name. + * + * @param q What to search for + * @param limit Maximum number of results. Defaults to 40. + * @param resolve Attempt WebFinger lookup. Defaults to false. Use this when q is an exact address. + * @param following Only who the user is following. Defaults to false. + * @return {@link LiveData} containing a {@link List} of matching {@link Account}s + */ + public LiveData> searchAccounts(@NonNull String instance, String token, @NonNull String q, int limit, boolean resolve, boolean following) { + accountListMutableLiveData = new MutableLiveData<>(); + MastodonAccountsService mastodonAccountsService = init(instance); + new Thread(() -> { + List accountList = null; + Call> searchCall = mastodonAccountsService.searchAccounts(token, q, limit, resolve, following); + if (searchCall != null) { + try { + Response> searchResponse = searchCall.execute(); + if (searchResponse.isSuccessful()) { + accountList = searchResponse.body(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + List finalAccountList = accountList; + if (finalAccountList != null) { + for (Account account : finalAccountList) { + SpannableHelper.convertAccount(getApplication().getApplicationContext(), account); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + Runnable myRunnable = () -> accountListMutableLiveData.setValue(finalAccountList); + mainHandler.post(myRunnable); + }).start(); + return accountListMutableLiveData; + } + + /** + * Statuses the user has bookmarked. + * + * @return {@link LiveData} containing a {@link List} of {@link Status}es + */ + public LiveData getBookmarks(@NonNull String instance, String token, String limit, String maxId, String sinceId, String minId) { + statusesMutableLiveData = new MutableLiveData<>(); + MastodonAccountsService mastodonAccountsService = init(instance); + new Thread(() -> { + List statusList; + Statuses statuses = new Statuses(); + Call> bookmarksCall = mastodonAccountsService.getBookmarks(token, limit, maxId, sinceId, minId); + if (bookmarksCall != null) { + try { + Response> bookmarksResponse = bookmarksCall.execute(); + if (bookmarksResponse.isSuccessful()) { + statusList = bookmarksResponse.body(); + statuses.statuses = SpannableHelper.convertStatus(getApplication().getApplicationContext(), statusList); + statuses.pagination = MastodonHelper.getPaginationStatus(statusList); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + Runnable myRunnable = () -> statusesMutableLiveData.setValue(statuses); + mainHandler.post(myRunnable); + }).start(); + return statusesMutableLiveData; + } + + /** + * Statuses the user has favourited. + * + * @return {@link LiveData} containing a {@link List} of {@link Status}es + */ + public LiveData getFavourites(@NonNull String instance, String token, String limit, String minId, String maxId) { + statusesMutableLiveData = new MutableLiveData<>(); + MastodonAccountsService mastodonAccountsService = init(instance); + new Thread(() -> { + Statuses statuses = new Statuses(); + Call> favouritesCall = mastodonAccountsService.getFavourites(token, limit, minId, maxId); + List statusList; + if (favouritesCall != null) { + try { + Response> favouritesResponse = favouritesCall.execute(); + if (favouritesResponse.isSuccessful()) { + statusList = favouritesResponse.body(); + statuses.statuses = SpannableHelper.convertStatus(getApplication().getApplicationContext(), statusList); + statuses.pagination = MastodonHelper.getPaginationStatus(statusList); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + Runnable myRunnable = () -> statusesMutableLiveData.setValue(statuses); + mainHandler.post(myRunnable); + }).start(); + return statusesMutableLiveData; + } + + /** + * Accounts the user has muted. + * + * @param limit Maximum number of results to return per page. Defaults to 40. + * @return {@link LiveData} containing a {@link List} of {@link Account}s + */ + public LiveData getMutes(@NonNull String instance, String token, String limit, String maxId, String sinceId) { + accountsMutableLiveData = new MutableLiveData<>(); + MastodonAccountsService mastodonAccountsService = init(instance); + new Thread(() -> { + Accounts accounts = new Accounts(); + Call> mutesCall = mastodonAccountsService.getMutes(token, limit, maxId, sinceId); + List accountList; + if (mutesCall != null) { + try { + Response> mutesResponse = mutesCall.execute(); + if (mutesResponse.isSuccessful()) { + accountList = mutesResponse.body(); + accounts.accounts = SpannableHelper.convertAccounts(getApplication().getApplicationContext(), accountList); + accounts.pagination = MastodonHelper.getPaginationAccount(accountList); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + Runnable myRunnable = () -> accountsMutableLiveData.setValue(accounts); + mainHandler.post(myRunnable); + }).start(); + return accountsMutableLiveData; + } + + /** + * Accounts the user has blocked. + * + * @param limit Maximum number of results. Defaults to 40. + * @return {@link LiveData} containing a {@link List} of {@link Account}s + */ + public LiveData getBlocks(@NonNull String instance, String token, String limit, String maxId, String sinceId) { + accountsMutableLiveData = new MutableLiveData<>(); + MastodonAccountsService mastodonAccountsService = init(instance); + new Thread(() -> { + List accountList; + Accounts accounts = new Accounts(); + Call> blocksCall = mastodonAccountsService.getBlocks(token, limit, maxId, sinceId); + if (blocksCall != null) { + try { + Response> blocksResponse = blocksCall.execute(); + if (blocksResponse.isSuccessful()) { + accountList = blocksResponse.body(); + accounts.accounts = SpannableHelper.convertAccounts(getApplication().getApplicationContext(), accountList); + accounts.pagination = MastodonHelper.getPaginationAccount(accountList); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + Runnable myRunnable = () -> accountsMutableLiveData.setValue(accounts); + mainHandler.post(myRunnable); + }).start(); + return accountsMutableLiveData; + } + + /** + * View domains the user has blocked. + * + * @param limit Maximum number of results. Defaults to 40. + * @return {@link LiveData} containing a {@link List} of {@link String}s + */ + public LiveData> getDomainBlocks(@NonNull String instance, String token, String limit, String maxId, String sinceId) { + stringListMutableLiveData = new MutableLiveData<>(); + MastodonAccountsService mastodonAccountsService = init(instance); + new Thread(() -> { + List stringList = null; + Call> getDomainBlocksCall = mastodonAccountsService.getDomainBlocks(token, limit, maxId, sinceId); + if (getDomainBlocksCall != null) { + try { + Response> getDomainBlocksResponse = getDomainBlocksCall.execute(); + if (getDomainBlocksResponse.isSuccessful()) { + stringList = getDomainBlocksResponse.body(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + List finalStringList = stringList; + Runnable myRunnable = () -> stringListMutableLiveData.setValue(finalStringList); + mainHandler.post(myRunnable); + }).start(); + return stringListMutableLiveData; + } + + /** + * block a domain to: + * • hide all public posts from it + * • hide all notifications from it + * • remove all followers from it + * • prevent following new users from it (but does not remove existing follows) + * + * @param domain Domain to block. + */ + public void addDomainBlocks(@NonNull String instance, String token, String domain) { + MastodonAccountsService mastodonAccountsService = init(instance); + new Thread(() -> { + Call addDomainBlockCall = mastodonAccountsService.addDomainBlock(token, domain); + if (addDomainBlockCall != null) { + try { + addDomainBlockCall.execute(); + } catch (IOException e) { + e.printStackTrace(); + } + } + }).start(); + } + + /** + * Remove a domain block, if it exists in the user's array of blocked domains. + * + * @param domain Domain to unblock. + */ + public void removeDomainBlocks(@NonNull String instance, String token, String domain) { + MastodonAccountsService mastodonAccountsService = init(instance); + new Thread(() -> { + Call removeDomainBlockCall = mastodonAccountsService.removeDomainBlocks(token, domain); + if (removeDomainBlockCall != null) { + try { + removeDomainBlockCall.execute(); + } catch (IOException e) { + e.printStackTrace(); + } + } + }).start(); + } + + /** + * View all filters created by the user + * + * @return {@link LiveData} containing a {@link List} of {@link Filter}s + */ + public LiveData> getFilters(@NonNull String instance, String token) { + filterListMutableLiveData = new MutableLiveData<>(); + MastodonAccountsService mastodonAccountsService = init(instance); + new Thread(() -> { + List filterList = null; + Call> getFiltersCall = mastodonAccountsService.getFilters(token); + if (getFiltersCall != null) { + try { + Response> getFiltersResponse = getFiltersCall.execute(); + if (getFiltersResponse.isSuccessful()) { + BaseMainActivity.filterFetched = true; + filterList = getFiltersResponse.body(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + List finalFilterList = filterList; + Runnable myRunnable = () -> filterListMutableLiveData.setValue(finalFilterList); + mainHandler.post(myRunnable); + }).start(); + return filterListMutableLiveData; + } + + /** + * View a single filter + * + * @param id the id of the filter + * @return {@link LiveData} containing a {@link Filter} + */ + public LiveData getFilter(@NonNull String instance, String token, @NonNull String id) { + filterMutableLiveData = new MutableLiveData<>(); + MastodonAccountsService mastodonAccountsService = init(instance); + new Thread(() -> { + Filter filter = null; + Call getFilterCall = mastodonAccountsService.getFilter(token, id); + if (getFilterCall != null) { + try { + Response getFiltersResponse = getFilterCall.execute(); + if (getFiltersResponse.isSuccessful()) { + filter = getFiltersResponse.body(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + Filter finalFilter = filter; + Runnable myRunnable = () -> filterMutableLiveData.setValue(finalFilter); + mainHandler.post(myRunnable); + }).start(); + return filterMutableLiveData; + } + + /** + * Create a filter + * + * @param phrase Text to be filtered + * @param filterContext Array of enumerable strings "home", "notifications", "public", "thread". At least one context must be specified. + * @param irreversible Should the server irreversibly drop matching entities from home and notifications? + * @param wholeWord Consider word boundaries? + * @param expiresIn Number of seconds from now the filter should expire. Otherwise, null for a filter that doesn't expire. + * @return {@link LiveData} containing a {@link Filter} + */ + public LiveData addFilter(@NonNull String instance, String token, + @NonNull String phrase, + @NonNull List filterContext, + boolean irreversible, + boolean wholeWord, + long expiresIn) { + filterMutableLiveData = new MutableLiveData<>(); + MastodonAccountsService mastodonAccountsService = init(instance); + new Thread(() -> { + Filter filter = null; + Call addFilterCall = mastodonAccountsService.addFilter(token, phrase, filterContext, irreversible, wholeWord, expiresIn); + if (addFilterCall != null) { + try { + Response addFiltersResponse = addFilterCall.execute(); + if (addFiltersResponse.isSuccessful()) { + filter = addFiltersResponse.body(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + Filter finalFilter = filter; + Runnable myRunnable = () -> filterMutableLiveData.setValue(finalFilter); + mainHandler.post(myRunnable); + }).start(); + return filterMutableLiveData; + } + + /** + * Update a filter + * + * @param id ID of the filter + * @param phrase Text to be filtered + * @param filterContext Array of enumerable strings "home", "notifications", "public", "thread". At least one context must be specified. + * @param irreversible Should the server irreversibly drop matching entities from home and notifications? + * @param wholeWord Consider word boundaries? + * @param expiresIn Number of seconds from now the filter should expire. Otherwise, null for a filter that doesn't expire. + * @return {@link LiveData} containing a {@link Filter} + */ + public LiveData editFilter(@NonNull String instance, String token, @NonNull String id, @NonNull String phrase, @NonNull List filterContext, boolean irreversible, boolean wholeWord, long expiresIn) { + filterMutableLiveData = new MutableLiveData<>(); + MastodonAccountsService mastodonAccountsService = init(instance); + new Thread(() -> { + Filter filter = null; + Call editFilterCall = mastodonAccountsService.editFilter(token, id, phrase, filterContext, irreversible, wholeWord, expiresIn); + if (editFilterCall != null) { + try { + Response editFiltersResponse = editFilterCall.execute(); + if (editFiltersResponse.isSuccessful()) { + filter = editFiltersResponse.body(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + Filter finalFilter = filter; + Runnable myRunnable = () -> filterMutableLiveData.setValue(finalFilter); + mainHandler.post(myRunnable); + }).start(); + return filterMutableLiveData; + } + + /** + * Remove a filter + * + * @param id ID of the filter + */ + public void removeFilter(@NonNull String instance, String token, @NonNull String id) { + MastodonAccountsService mastodonAccountsService = init(instance); + new Thread(() -> { + Call removeFilterCall = mastodonAccountsService.removeFilter(token, id); + if (removeFilterCall != null) { + try { + removeFilterCall.execute(); + } catch (IOException e) { + e.printStackTrace(); + } + } + }).start(); + } + + /** + * File a report + * + * @param accountId ID of the account to report + * @param statusIds {@link List} of IDs of statuses to attach to the report, for context + * @param comment Reason for the report (default max 1000 characters) + * @param forward If the account is remote, should the report be forwarded to the remote admin? + */ + public LiveData report(@NonNull String instance, String token, @NonNull String accountId, String category, List statusIds, List ruleIds, String comment, Boolean forward) { + reportMutableLiveData = new MutableLiveData<>(); + MastodonAccountsService mastodonAccountsService = init(instance); + new Thread(() -> { + Report report = null; + Report.ReportParams reportParams = new Report.ReportParams(); + reportParams.account_id = accountId; + reportParams.category = category; + reportParams.comment = comment; + reportParams.forward = forward; + reportParams.rule_ids = ruleIds; + reportParams.status_ids = statusIds; + Call reportCall = mastodonAccountsService.report(token, reportParams); + if (reportCall != null) { + try { + Response reportRequestsResponse = reportCall.execute(); + if (reportRequestsResponse.isSuccessful()) { + report = reportRequestsResponse.body(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + Report finalReport = report; + Runnable myRunnable = () -> reportMutableLiveData.setValue(finalReport); + mainHandler.post(myRunnable); + }).start(); + return reportMutableLiveData; + } + + /** + * View pending follow requests + * + * @param limit Maximum number of results to return. Defaults to 40. + * @return {@link LiveData} containing a {@link List} of {@link Account}s + */ + public LiveData> getFollowRequests(@NonNull String instance, String token, String limit) { + accountListMutableLiveData = new MutableLiveData<>(); + MastodonAccountsService mastodonAccountsService = init(instance); + new Thread(() -> { + List accountList = null; + Call> followRequestsCall = mastodonAccountsService.getFollowRequests(token, limit); + if (followRequestsCall != null) { + try { + Response> followRequestsResponse = followRequestsCall.execute(); + if (followRequestsResponse.isSuccessful()) { + accountList = followRequestsResponse.body(); + } + } catch (IOException e) { + e.printStackTrace(); + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + List finalAccountList = accountList; + Runnable myRunnable = () -> accountListMutableLiveData.setValue(finalAccountList); + mainHandler.post(myRunnable); + } + }).start(); + return accountListMutableLiveData; + } + + /** + * Accept a pending follow requests + * + * @param id ID of the account + * @return {@link LiveData} containing the {@link RelationShip} to the given account + */ + public LiveData acceptFollow(@NonNull String instance, String token, String id) { + relationShipMutableLiveData = new MutableLiveData<>(); + MastodonAccountsService mastodonAccountsService = init(instance); + new Thread(() -> { + RelationShip relationShip = null; + Call acceptFollowCall = mastodonAccountsService.acceptFollow(token, id); + if (acceptFollowCall != null) { + try { + Response acceptFollowResponse = acceptFollowCall.execute(); + if (acceptFollowResponse.isSuccessful()) { + relationShip = acceptFollowResponse.body(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + RelationShip finalRelationShip = relationShip; + Runnable myRunnable = () -> relationShipMutableLiveData.setValue(finalRelationShip); + mainHandler.post(myRunnable); + }).start(); + return relationShipMutableLiveData; + } + + /** + * Reject a pending follow requests + * + * @param id ID of the account + * @return {@link LiveData} containing the {@link RelationShip} to the given account + */ + public LiveData rejectFollow(@NonNull String instance, String token, String id) { + relationShipMutableLiveData = new MutableLiveData<>(); + MastodonAccountsService mastodonAccountsService = init(instance); + new Thread(() -> { + RelationShip relationShip = null; + Call rejectFollowCall = mastodonAccountsService.rejectFollow(token, id); + if (rejectFollowCall != null) { + try { + Response rejectFollowResponse = rejectFollowCall.execute(); + if (rejectFollowResponse.isSuccessful()) { + relationShip = rejectFollowResponse.body(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + RelationShip finalRelationShip = relationShip; + Runnable myRunnable = () -> relationShipMutableLiveData.setValue(finalRelationShip); + mainHandler.post(myRunnable); + }).start(); + return relationShipMutableLiveData; + } + + /** + * View accounts that the user is currently featuring on their profile. + * + * @param limit Maximum number of results to return. Defaults to 40. + * @return {@link LiveData} containing a {@link List} of {@link Account}s + */ + public LiveData> getEndorsements(@NonNull String instance, String token, String limit, String maxId, String sinceId) { + accountListMutableLiveData = new MutableLiveData<>(); + MastodonAccountsService mastodonAccountsService = init(instance); + new Thread(() -> { + List accountList = null; + Call> endorsementsCall = mastodonAccountsService.getEndorsements(token, limit, maxId, sinceId); + if (endorsementsCall != null) { + try { + Response> endorsementsResponse = endorsementsCall.execute(); + if (endorsementsResponse.isSuccessful()) { + accountList = endorsementsResponse.body(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + List finalAccountList = accountList; + Runnable myRunnable = () -> accountListMutableLiveData.setValue(finalAccountList); + mainHandler.post(myRunnable); + }).start(); + return accountListMutableLiveData; + } + + /** + * View your featured tags + * + * @return {@link LiveData} containing a {@link List} of {@link FeaturedTag}s + */ + public LiveData> getFeaturedTags(@NonNull String instance, String token) { + featuredTagListMutableLiveData = new MutableLiveData<>(); + MastodonAccountsService mastodonAccountsService = init(instance); + new Thread(() -> { + List featuredTagList = null; + Call> getFeaturedTagsCall = mastodonAccountsService.getFeaturedTags(token); + if (getFeaturedTagsCall != null) { + try { + Response> getFeaturedTagsResponse = getFeaturedTagsCall.execute(); + if (getFeaturedTagsResponse.isSuccessful()) { + featuredTagList = getFeaturedTagsResponse.body(); + } + } catch (IOException e) { + e.printStackTrace(); + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + List finalFeaturedTagList = featuredTagList; + Runnable myRunnable = () -> featuredTagListMutableLiveData.setValue(finalFeaturedTagList); + mainHandler.post(myRunnable); + } + }).start(); + return featuredTagListMutableLiveData; + } + + /** + * Feature a tag + * + * @param name The hashtag to be featured. + * @return {@link LiveData} containing a {@link FeaturedTag} + */ + public LiveData addFeaturedTag(@NonNull String instance, String token, @NonNull String name) { + featuredTagMutableLiveData = new MutableLiveData<>(); + MastodonAccountsService mastodonAccountsService = init(instance); + new Thread(() -> { + FeaturedTag featuredTag = null; + Call addFeaturedTagCall = mastodonAccountsService.addFeaturedTag(token, name); + if (addFeaturedTagCall != null) { + try { + Response addFeaturedTagResponse = addFeaturedTagCall.execute(); + if (addFeaturedTagResponse.isSuccessful()) { + featuredTag = addFeaturedTagResponse.body(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + FeaturedTag finalFeaturedTag = featuredTag; + Runnable myRunnable = () -> featuredTagMutableLiveData.setValue(finalFeaturedTag); + mainHandler.post(myRunnable); + }).start(); + return featuredTagMutableLiveData; + } + + /** + * Unfeature a tag + * + * @param id The id of the FeaturedTag to be unfeatured. + */ + public void removeFeaturedTag(@NonNull String instance, String token, String id) { + MastodonAccountsService mastodonAccountsService = init(instance); + new Thread(() -> { + Call removeFeaturedTagCall = mastodonAccountsService.removeFeaturedTag(token, id); + if (removeFeaturedTagCall != null) { + try { + removeFeaturedTagCall.execute(); + } catch (IOException e) { + e.printStackTrace(); + } + } + }).start(); + } + + /** + * Shows your 10 most-used tags, with usage history for the past week. + * + * @return {@link LiveData} containing a {@link List} of {@link Tag}s + */ + public LiveData> getFeaturedTagsSuggestions(@NonNull String instance, String token) { + tagListMutableLiveData = new MutableLiveData<>(); + MastodonAccountsService mastodonAccountsService = init(instance); + new Thread(() -> { + List tagList = null; + Call> featuredTagsSuggestionsCall = mastodonAccountsService.getFeaturedTagsSuggestions(token); + if (featuredTagsSuggestionsCall != null) { + try { + Response> featuredTagsSuggestionsResponse = featuredTagsSuggestionsCall.execute(); + if (featuredTagsSuggestionsResponse.isSuccessful()) { + tagList = featuredTagsSuggestionsResponse.body(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + List finalTagList = tagList; + Runnable myRunnable = () -> tagListMutableLiveData.setValue(finalTagList); + mainHandler.post(myRunnable); + }).start(); + return tagListMutableLiveData; + } + + /** + * Preferences defined by the user in their account settings. + * + * @return {@link LiveData} containing {@link Preferences} + */ + public LiveData getPreferences(@NonNull String instance, String token) { + preferencesMutableLiveData = new MutableLiveData<>(); + MastodonAccountsService mastodonAccountsService = init(instance); + new Thread(() -> { + Preferences preferences = null; + Call preferencesCall = mastodonAccountsService.getPreferences(token); + if (preferencesCall != null) { + try { + Response preferencesResponse = preferencesCall.execute(); + if (preferencesResponse.isSuccessful()) { + preferences = preferencesResponse.body(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + Preferences finalPreferences = preferences; + Runnable myRunnable = () -> preferencesMutableLiveData.setValue(finalPreferences); + mainHandler.post(myRunnable); + }).start(); + return preferencesMutableLiveData; + } + + /** + * Accounts the user has had past positive interactions with, but is not yet following. + * + * @param limit Maximum number of results to return. Defaults to 40. + * @return {@link LiveData} containing a {@link List} of {@link Account}s + */ + public LiveData> getSuggestions(@NonNull String instance, String token, String limit) { + accountListMutableLiveData = new MutableLiveData<>(); + MastodonAccountsService mastodonAccountsService = init(instance); + new Thread(() -> { + List accountList = null; + Call> suggestionsCall = mastodonAccountsService.getSuggestions(token, limit); + if (suggestionsCall != null) { + try { + Response> suggestionsResponse = suggestionsCall.execute(); + if (suggestionsResponse.isSuccessful()) { + accountList = suggestionsResponse.body(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + List finalAccountList = accountList; + Runnable myRunnable = () -> accountListMutableLiveData.setValue(finalAccountList); + mainHandler.post(myRunnable); + }).start(); + return accountListMutableLiveData; + } + + /** + * Remove an account from follow suggestions. + * + * @param accountId id of the account in the database to be removed from suggestions + */ + public void removeSuggestion(@NonNull String instance, String token, String accountId) { + MastodonAccountsService mastodonAccountsService = init(instance); + new Thread(() -> { + Call removeSuggestionCall = mastodonAccountsService.removeSuggestion(token, accountId); + if (removeSuggestionCall != null) { + try { + removeSuggestionCall.execute(); + } catch (IOException e) { + e.printStackTrace(); + } + } + }).start(); + } + + public enum UpdateMediaType { + AVATAR, + HEADER + } +} diff --git a/app/src/main/java/app/fedilab/android/viewmodel/mastodon/AdminVM.java b/app/src/main/java/app/fedilab/android/viewmodel/mastodon/AdminVM.java new file mode 100644 index 000000000..82332af8d --- /dev/null +++ b/app/src/main/java/app/fedilab/android/viewmodel/mastodon/AdminVM.java @@ -0,0 +1,542 @@ +package app.fedilab.android.viewmodel.mastodon; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import android.app.Application; +import android.os.Handler; +import android.os.Looper; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import java.io.IOException; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import app.fedilab.android.client.mastodon.MastodonAdminService; +import app.fedilab.android.client.mastodon.entities.AdminAccount; +import app.fedilab.android.client.mastodon.entities.AdminReport; +import app.fedilab.android.helper.Helper; +import okhttp3.OkHttpClient; +import retrofit2.Call; +import retrofit2.Response; +import retrofit2.Retrofit; +import retrofit2.converter.gson.GsonConverterFactory; + +public class AdminVM extends AndroidViewModel { + + final OkHttpClient okHttpClient = new OkHttpClient.Builder() + .readTimeout(60, TimeUnit.SECONDS) + .connectTimeout(60, TimeUnit.SECONDS) + .proxy(Helper.getProxy(getApplication().getApplicationContext())) + .build(); + private MutableLiveData adminAccountMutableLiveData; + private MutableLiveData> adminAccountListMutableLiveData; + private MutableLiveData adminReportMutableLiveData; + private MutableLiveData> adminReportListMutableLiveData; + + public AdminVM(@NonNull Application application) { + super(application); + } + + private MastodonAdminService init(@NonNull String instance) { + Retrofit retrofit = new Retrofit.Builder() + .baseUrl("https://" + instance + "/api/v1/") + .addConverterFactory(GsonConverterFactory.create()) + .client(okHttpClient) + .build(); + return retrofit.create(MastodonAdminService.class); + } + + /** + * View accounts matching certain criteria for filtering, up to 100 at a time. + * + * @param instance Instance domain of the active account + * @param token Access token of the active account + * @param local Filter for local accounts? + * @param remote Filter for remote accounts? + * @param byDomain Filter by the given domain + * @param active Filter for currently active accounts? + * @param pending Filter for currently pending accounts? + * @param disabled Filter for currently disabled accounts? + * @param silenced Filter for currently silenced accounts? + * @param suspended Filter for currently suspended accounts? + * @param username Username to search for + * @param displayName Display name to search for + * @param email Lookup a user with this email + * @param ip Lookup users by this IP address + * @param staff Filter for staff accounts? + * @return {@link LiveData} containing a {@link List} of {@link AdminAccount}s + */ + private LiveData> getAccounts(@NonNull String instance, + String token, + boolean local, + boolean remote, + String byDomain, + boolean active, + boolean pending, + boolean disabled, + boolean silenced, + boolean suspended, + String username, + String displayName, + String email, + String ip, + boolean staff, + String maxId, + String sinceId, + int limit) { + MastodonAdminService mastodonAdminService = init(instance); + adminAccountListMutableLiveData = new MutableLiveData<>(); + new Thread(() -> { + List adminAccountList = null; + Call> getAccountsCall = mastodonAdminService.getAccounts( + token, local, remote, byDomain, active, pending, disabled, silenced, suspended, + username, displayName, email, ip, staff, maxId, sinceId, limit); + if (getAccountsCall != null) { + try { + Response> getAccountsResponse = getAccountsCall.execute(); + if (getAccountsResponse.isSuccessful()) { + adminAccountList = getAccountsResponse.body(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + List finalAdminAccountList = adminAccountList; + Runnable myRunnable = () -> adminAccountListMutableLiveData.setValue(finalAdminAccountList); + mainHandler.post(myRunnable); + }).start(); + return adminAccountListMutableLiveData; + } + + /** + * View admin-level information about the given account. + * + * @param instance Instance domain of the active account + * @param token Access token of the active account + * @param id ID of the account + * @return {@link LiveData} containing a {@link AdminAccount} + */ + public LiveData getAccount(@NonNull String instance, String token, @NonNull String id) { + MastodonAdminService mastodonAdminService = init(instance); + adminAccountMutableLiveData = new MutableLiveData<>(); + new Thread(() -> { + AdminAccount adminAccount = null; + Call getAccountCall = mastodonAdminService.getAccount(token, id); + if (getAccountCall != null) { + try { + Response getAccountResponse = getAccountCall.execute(); + if (getAccountResponse.isSuccessful()) { + adminAccount = getAccountResponse.body(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + AdminAccount finalAdminAccount = adminAccount; + Runnable myRunnable = () -> adminAccountMutableLiveData.setValue(finalAdminAccount); + mainHandler.post(myRunnable); + }).start(); + return adminAccountMutableLiveData; + } + + /** + * Perform an action against an account and log this action in the moderation history. + * + * @param instance Instance domain of the active account + * @param token Access token of the active account + * @param accountId ID of the account + * @param type Type of action to be taken. Enumerable oneOf: "none" "disable" "silence" "suspend" + * @param reportId ID of an associated report that caused this action to be taken + * @param warningPresetId ID of a preset warning + * @param text Additional text for clarification of why this action was taken + * @param sendEmailNotification Whether an email should be sent to the user with the above information. + */ + public void performAction(@NonNull String instance, + String token, + @NonNull String accountId, + String type, + String reportId, + String warningPresetId, + String text, + boolean sendEmailNotification) { + MastodonAdminService mastodonAdminService = init(instance); + new Thread(() -> { + Call performActionCall = mastodonAdminService.performAction(token, accountId, type, reportId, warningPresetId, text, sendEmailNotification); + if (performActionCall != null) { + try { + performActionCall.execute(); + } catch (IOException e) { + e.printStackTrace(); + } + } + }).start(); + } + + /** + * Approve the given local account if it is currently pending approval. + * + * @param instance Instance domain of the active account + * @param token Access token of the active account + * @param accountId ID of the account + * @return {@link LiveData} containing a {@link AdminAccount} + */ + public LiveData approve(@NonNull String instance, String token, @NonNull String accountId) { + MastodonAdminService mastodonAdminService = init(instance); + adminAccountMutableLiveData = new MutableLiveData<>(); + new Thread(() -> { + AdminAccount adminAccount = null; + Call approveCall = mastodonAdminService.approve(token, accountId); + if (approveCall != null) { + try { + Response approveResponse = approveCall.execute(); + if (approveResponse.isSuccessful()) { + adminAccount = approveResponse.body(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + AdminAccount finalAdminAccount = adminAccount; + Runnable myRunnable = () -> adminAccountMutableLiveData.setValue(finalAdminAccount); + mainHandler.post(myRunnable); + }).start(); + return adminAccountMutableLiveData; + } + + /** + * Reject the given local account if it is currently pending approval. + * + * @param instance Instance domain of the active account + * @param token Access token of the active account + * @param accountId ID of the account + * @return {@link LiveData} containing a {@link AdminAccount} + */ + public LiveData reject(@NonNull String instance, String token, @NonNull String accountId) { + MastodonAdminService mastodonAdminService = init(instance); + adminAccountMutableLiveData = new MutableLiveData<>(); + new Thread(() -> { + AdminAccount adminAccount = null; + Call rejectCall = mastodonAdminService.reject(token, accountId); + if (rejectCall != null) { + try { + Response rejectResponse = rejectCall.execute(); + if (rejectResponse.isSuccessful()) { + adminAccount = rejectResponse.body(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + AdminAccount finalAdminAccount = adminAccount; + Runnable myRunnable = () -> adminAccountMutableLiveData.setValue(finalAdminAccount); + mainHandler.post(myRunnable); + }).start(); + return adminAccountMutableLiveData; + } + + /** + * Re-enable a local account whose login is currently disabled. + * + * @param instance Instance domain of the active account + * @param token Access token of the active account + * @param accountId ID of the account + * @return {@link LiveData} containing a {@link AdminAccount} + */ + public LiveData enable(@NonNull String instance, String token, @NonNull String accountId) { + MastodonAdminService mastodonAdminService = init(instance); + adminAccountMutableLiveData = new MutableLiveData<>(); + new Thread(() -> { + AdminAccount adminAccount = null; + Call enableCall = mastodonAdminService.enable(token, accountId); + if (enableCall != null) { + try { + Response enableResponse = enableCall.execute(); + if (enableResponse.isSuccessful()) { + adminAccount = enableResponse.body(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + AdminAccount finalAdminAccount = adminAccount; + Runnable myRunnable = () -> adminAccountMutableLiveData.setValue(finalAdminAccount); + mainHandler.post(myRunnable); + }).start(); + return adminAccountMutableLiveData; + } + + /** + * Unsilence a currently silenced account. + * + * @param instance Instance domain of the active account + * @param token Access token of the active account + * @param accountId ID of the account + * @return {@link LiveData} containing a {@link AdminAccount} + */ + public LiveData unsilence(@NonNull String instance, String token, @NonNull String accountId) { + MastodonAdminService mastodonAdminService = init(instance); + adminAccountMutableLiveData = new MutableLiveData<>(); + new Thread(() -> { + AdminAccount adminAccount = null; + Call unsilenceCall = mastodonAdminService.unsilence(token, accountId); + if (unsilenceCall != null) { + try { + Response unsilenceResponse = unsilenceCall.execute(); + if (unsilenceResponse.isSuccessful()) { + adminAccount = unsilenceResponse.body(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + AdminAccount finalAdminAccount = adminAccount; + Runnable myRunnable = () -> adminAccountMutableLiveData.setValue(finalAdminAccount); + mainHandler.post(myRunnable); + }).start(); + return adminAccountMutableLiveData; + } + + /** + * Unsuspend a currently suspended account. + * + * @param instance Instance domain of the active account + * @param token Access token of the active account + * @param accountId ID of the account + * @return {@link LiveData} containing a {@link AdminAccount} + */ + public LiveData unsuspend(@NonNull String instance, String token, @NonNull String accountId) { + MastodonAdminService mastodonAdminService = init(instance); + adminAccountMutableLiveData = new MutableLiveData<>(); + new Thread(() -> { + AdminAccount adminAccount = null; + Call unsuspendCall = mastodonAdminService.unsuspend(token, accountId); + if (unsuspendCall != null) { + try { + Response unsuspendResponse = unsuspendCall.execute(); + if (unsuspendResponse.isSuccessful()) { + adminAccount = unsuspendResponse.body(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + AdminAccount finalAdminAccount = adminAccount; + Runnable myRunnable = () -> adminAccountMutableLiveData.setValue(finalAdminAccount); + mainHandler.post(myRunnable); + }).start(); + return adminAccountMutableLiveData; + } + + /** + * View all reports. + * + * @param instance Instance domain of the active account + * @param token Access token of the active account + * @return {@link LiveData} containing a {@link List} of {@link AdminReport}s + */ + public LiveData> getReports(@NonNull String instance, + String token, + boolean resolved, + String accountId, + String targetAccountId) { + MastodonAdminService mastodonAdminService = init(instance); + adminReportListMutableLiveData = new MutableLiveData<>(); + new Thread(() -> { + List adminReportList = null; + Call> getReportsCall = mastodonAdminService.getReports(token, resolved, accountId, targetAccountId); + if (getReportsCall != null) { + try { + Response> getReportsResponse = getReportsCall.execute(); + if (getReportsResponse.isSuccessful()) { + adminReportList = getReportsResponse.body(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + List finalAdminReportList = adminReportList; + Runnable myRunnable = () -> adminReportListMutableLiveData.setValue(finalAdminReportList); + mainHandler.post(myRunnable); + }).start(); + return adminReportListMutableLiveData; + } + + /** + * View information about the report with the given ID. + * + * @param instance Instance domain of the active account + * @param token Access token of the active account + * @return {@link LiveData} containing a {@link AdminReport} + */ + public LiveData getReport(@NonNull String instance, String token, @NonNull String id) { + MastodonAdminService mastodonAdminService = init(instance); + adminReportMutableLiveData = new MutableLiveData<>(); + new Thread(() -> { + AdminReport adminReport = null; + Call getReportCall = mastodonAdminService.getReport(token, id); + if (getReportCall != null) { + try { + Response getReportResponse = getReportCall.execute(); + if (getReportResponse.isSuccessful()) { + adminReport = getReportResponse.body(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + AdminReport finalAdminReportList = adminReport; + Runnable myRunnable = () -> adminReportMutableLiveData.setValue(finalAdminReportList); + mainHandler.post(myRunnable); + }).start(); + return adminReportMutableLiveData; + } + + /** + * Claim the handling of this report to yourself. + * + * @param instance Instance domain of the active account + * @param token Access token of the active account + * @return {@link LiveData} containing a {@link AdminReport} + */ + public LiveData assignToSelf(@NonNull String instance, String token, @NonNull String id) { + MastodonAdminService mastodonAdminService = init(instance); + adminReportMutableLiveData = new MutableLiveData<>(); + new Thread(() -> { + AdminReport adminReport = null; + Call assignToSelfCall = mastodonAdminService.assignToSelf(token, id); + if (assignToSelfCall != null) { + try { + Response assignToSelfResponse = assignToSelfCall.execute(); + if (assignToSelfResponse.isSuccessful()) { + adminReport = assignToSelfResponse.body(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + AdminReport finalAdminReportList = adminReport; + Runnable myRunnable = () -> adminReportMutableLiveData.setValue(finalAdminReportList); + mainHandler.post(myRunnable); + }).start(); + return adminReportMutableLiveData; + } + + /** + * Unassign a report so that someone else can claim it. + * + * @param instance Instance domain of the active account + * @param token Access token of the active account + * @return {@link LiveData} containing a {@link AdminReport} + */ + public LiveData unassign(@NonNull String instance, String token, @NonNull String id) { + MastodonAdminService mastodonAdminService = init(instance); + adminReportMutableLiveData = new MutableLiveData<>(); + new Thread(() -> { + AdminReport adminReport = null; + Call unassignCall = mastodonAdminService.unassign(token, id); + if (unassignCall != null) { + try { + Response unassignResponse = unassignCall.execute(); + if (unassignResponse.isSuccessful()) { + adminReport = unassignResponse.body(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + AdminReport finalAdminReportList = adminReport; + Runnable myRunnable = () -> adminReportMutableLiveData.setValue(finalAdminReportList); + mainHandler.post(myRunnable); + }).start(); + return adminReportMutableLiveData; + } + + /** + * Mark a report as resolved with no further action taken. + * + * @param instance Instance domain of the active account + * @param token Access token of the active account + * @return {@link LiveData} containing a {@link AdminReport} + */ + public LiveData resolved(@NonNull String instance, String token, @NonNull String id) { + MastodonAdminService mastodonAdminService = init(instance); + adminReportMutableLiveData = new MutableLiveData<>(); + new Thread(() -> { + AdminReport adminReport = null; + Call resolvedCall = mastodonAdminService.resolved(token, id); + if (resolvedCall != null) { + try { + Response resolvedResponse = resolvedCall.execute(); + if (resolvedResponse.isSuccessful()) { + adminReport = resolvedResponse.body(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + AdminReport finalAdminReportList = adminReport; + Runnable myRunnable = () -> adminReportMutableLiveData.setValue(finalAdminReportList); + mainHandler.post(myRunnable); + }).start(); + return adminReportMutableLiveData; + } + + /** + * Reopen a currently closed report. + * + * @param instance Instance domain of the active account + * @param token Access token of the active account + * @return {@link LiveData} containing a {@link AdminReport} + */ + public LiveData reopen(@NonNull String instance, String token, @NonNull String id) { + MastodonAdminService mastodonAdminService = init(instance); + adminReportMutableLiveData = new MutableLiveData<>(); + new Thread(() -> { + AdminReport adminReport = null; + Call reopenCall = mastodonAdminService.reopen(token, id); + if (reopenCall != null) { + try { + Response reopenResponse = reopenCall.execute(); + if (reopenResponse.isSuccessful()) { + adminReport = reopenResponse.body(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + AdminReport finalAdminReportList = adminReport; + Runnable myRunnable = () -> adminReportMutableLiveData.setValue(finalAdminReportList); + mainHandler.post(myRunnable); + }).start(); + return adminReportMutableLiveData; + } +} diff --git a/app/src/main/java/app/fedilab/android/viewmodel/mastodon/AnnouncementsVM.java b/app/src/main/java/app/fedilab/android/viewmodel/mastodon/AnnouncementsVM.java new file mode 100644 index 000000000..c16c758b7 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/viewmodel/mastodon/AnnouncementsVM.java @@ -0,0 +1,158 @@ +package app.fedilab.android.viewmodel.mastodon; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import android.app.Application; +import android.os.Handler; +import android.os.Looper; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import java.io.IOException; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import app.fedilab.android.client.mastodon.MastodonAnnouncementsService; +import app.fedilab.android.client.mastodon.entities.Announcement; +import app.fedilab.android.helper.Helper; +import okhttp3.OkHttpClient; +import retrofit2.Call; +import retrofit2.Response; +import retrofit2.Retrofit; +import retrofit2.converter.gson.GsonConverterFactory; + +public class AnnouncementsVM extends AndroidViewModel { + + final OkHttpClient okHttpClient = new OkHttpClient.Builder() + .readTimeout(60, TimeUnit.SECONDS) + .connectTimeout(60, TimeUnit.SECONDS) + .proxy(Helper.getProxy(getApplication().getApplicationContext())) + .build(); + private MutableLiveData announcementMutableLiveData; + private MutableLiveData> announcementListMutableLiveData; + + public AnnouncementsVM(@NonNull Application application) { + super(application); + } + + private MastodonAnnouncementsService init(@NonNull String instance) { + Retrofit retrofit = new Retrofit.Builder() + .baseUrl("https://" + instance + "/api/v1/") + .addConverterFactory(GsonConverterFactory.create()) + .client(okHttpClient) + .build(); + return retrofit.create(MastodonAnnouncementsService.class); + } + + /** + * See all currently active announcements set by admins. + * + * @param instance Instance domain of the active account + * @param token Access token of the active account + * @param withDismissed If true, response will include announcements dismissed by the user. Defaults to false. + * @return {@link LiveData} containing a {@link List} of {@link Announcement}s + */ + public LiveData> getAnnouncements(@NonNull String instance, String token, boolean withDismissed) { + MastodonAnnouncementsService mastodonAnnouncementsService = init(instance); + announcementListMutableLiveData = new MutableLiveData<>(); + new Thread(() -> { + List announcementList = null; + Call> getAnnouncementsCall = mastodonAnnouncementsService.getAnnouncements(token, withDismissed); + if (getAnnouncementsCall != null) { + try { + Response> getAnnouncementsResponse = getAnnouncementsCall.execute(); + if (getAnnouncementsResponse.isSuccessful()) { + announcementList = getAnnouncementsResponse.body(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + List finalAnnouncementList = announcementList; + Runnable myRunnable = () -> announcementListMutableLiveData.setValue(finalAnnouncementList); + mainHandler.post(myRunnable); + }).start(); + return announcementListMutableLiveData; + } + + /** + * Allows a user to mark the announcement as read. + * + * @param instance Instance domain of the active account + * @param token Access token of the active account + * @param id Local ID of an announcement + */ + public void dismiss(@NonNull String instance, String token, @NonNull String id) { + MastodonAnnouncementsService mastodonAnnouncementsService = init(instance); + new Thread(() -> { + Call dismissCall = mastodonAnnouncementsService.dismiss(token, id); + if (dismissCall != null) { + try { + dismissCall.execute(); + } catch (IOException e) { + e.printStackTrace(); + } + } + }).start(); + } + + /** + * React to an announcement with an emoji. + * + * @param instance Instance domain of the active account + * @param token Access token of the active account + * @param id Local ID of an announcement + * @param name Unicode emoji, or shortcode of custom emoji + */ + public void addReaction(@NonNull String instance, String token, @NonNull String id, @NonNull String name) { + MastodonAnnouncementsService mastodonAnnouncementsService = init(instance); + new Thread(() -> { + Call addReactionCall = mastodonAnnouncementsService.addReaction(token, id, name); + if (addReactionCall != null) { + try { + addReactionCall.execute(); + } catch (IOException e) { + e.printStackTrace(); + } + } + }).start(); + } + + /** + * Undo a react emoji to an announcement. + * + * @param instance Instance domain of the active account + * @param token Access token of the active account + * @param id Local ID of an announcement + * @param name Unicode emoji, or shortcode of custom emoji + */ + public void removeReaction(@NonNull String instance, String token, @NonNull String id, @NonNull String name) { + MastodonAnnouncementsService mastodonAnnouncementsService = init(instance); + new Thread(() -> { + Call removeReactionCall = mastodonAnnouncementsService.removeReaction(token, id, name); + if (removeReactionCall != null) { + try { + removeReactionCall.execute(); + } catch (IOException e) { + e.printStackTrace(); + } + } + }).start(); + } +} diff --git a/app/src/main/java/app/fedilab/android/viewmodel/mastodon/AppsVM.java b/app/src/main/java/app/fedilab/android/viewmodel/mastodon/AppsVM.java new file mode 100644 index 000000000..d923e73b7 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/viewmodel/mastodon/AppsVM.java @@ -0,0 +1,132 @@ +package app.fedilab.android.viewmodel.mastodon; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import android.app.Application; +import android.os.Handler; +import android.os.Looper; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +import app.fedilab.android.client.mastodon.MastodonAppsService; +import app.fedilab.android.client.mastodon.entities.App; +import app.fedilab.android.helper.Helper; +import okhttp3.OkHttpClient; +import retrofit2.Call; +import retrofit2.Response; +import retrofit2.Retrofit; +import retrofit2.converter.gson.GsonConverterFactory; + +public class AppsVM extends AndroidViewModel { + + final OkHttpClient okHttpClient = new OkHttpClient.Builder() + .readTimeout(60, TimeUnit.SECONDS) + .connectTimeout(60, TimeUnit.SECONDS) + .proxy(Helper.getProxy(getApplication().getApplicationContext())) + .build(); + private MutableLiveData appMutableLiveData; + + + /** + * Constructor - String token can be for the app or the account + * + * @param application Application + */ + public AppsVM(@NonNull Application application) { + super(application); + } + + private MastodonAppsService init(String instance) { + Retrofit retrofit = new Retrofit.Builder() + .baseUrl("https://" + instance + "/api/v1/") + .addConverterFactory(GsonConverterFactory.create()) + .client(okHttpClient) + .build(); + return retrofit.create(MastodonAppsService.class); + } + + /** + * Register client applications that can be used to obtain OAuth tokens. + * + * @param client_name String + * @param redirect_uris String + * @param scopes String + * @param website String + * @return {@link LiveData} containing an {@link App} + */ + public LiveData createApp(@NonNull String instance, String client_name, + String redirect_uris, + String scopes, + String website) { + appMutableLiveData = new MutableLiveData<>(); + MastodonAppsService mastodonAppsService = init(instance); + new Thread(() -> { + App app = null; + Call appCall = mastodonAppsService.createApp(client_name, redirect_uris, scopes, website); + if (appCall != null) { + try { + Response appResponse = appCall.execute(); + if (appResponse.isSuccessful()) { + app = appResponse.body(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + App finalApp = app; + Runnable myRunnable = () -> appMutableLiveData.setValue(finalApp); + mainHandler.post(myRunnable); + }).start(); + return appMutableLiveData; + } + + /** + * Confirm that the app's OAuth2 credentials work. + * + * @return {@link LiveData} containing an {@link App} + */ + public LiveData verifyCredentials(@NonNull String instance, String token) { + MastodonAppsService mastodonAppsService = init(instance); + appMutableLiveData = new MutableLiveData<>(); + new Thread(() -> { + App app = null; + Call appCall = mastodonAppsService.verifyCredentials(token); + if (appCall != null) { + try { + Response appResponse = appCall.execute(); + if (appResponse.isSuccessful()) { + app = appResponse.body(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + App finalApp = app; + Runnable myRunnable = () -> appMutableLiveData.setValue(finalApp); + mainHandler.post(myRunnable); + }).start(); + return appMutableLiveData; + } + + +} diff --git a/app/src/main/java/app/fedilab/android/viewmodel/mastodon/InstanceSocialVM.java b/app/src/main/java/app/fedilab/android/viewmodel/mastodon/InstanceSocialVM.java new file mode 100644 index 000000000..db6b0262f --- /dev/null +++ b/app/src/main/java/app/fedilab/android/viewmodel/mastodon/InstanceSocialVM.java @@ -0,0 +1,86 @@ +package app.fedilab.android.viewmodel.mastodon; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import android.app.Application; +import android.os.Handler; +import android.os.Looper; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.MutableLiveData; + +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +import app.fedilab.android.InstancesSocialService; +import app.fedilab.android.client.entities.InstanceSocial; +import app.fedilab.android.helper.Helper; +import okhttp3.OkHttpClient; +import retrofit2.Call; +import retrofit2.Response; +import retrofit2.Retrofit; +import retrofit2.converter.gson.GsonConverterFactory; + +public class InstanceSocialVM extends AndroidViewModel { + + final OkHttpClient okHttpClient = new OkHttpClient.Builder() + .readTimeout(60, TimeUnit.SECONDS) + .connectTimeout(60, TimeUnit.SECONDS) + .proxy(Helper.getProxy(getApplication().getApplicationContext())) + .build(); + private final InstancesSocialService instancesSocialService; + private MutableLiveData instanceSocialMutableLiveData; + + + public InstanceSocialVM(@NonNull Application application) { + super(application); + instancesSocialService = init(); + } + + private InstancesSocialService init() { + Retrofit retrofit = new Retrofit.Builder() + .baseUrl("https://instances.social/api/1.0/") + .addConverterFactory(GsonConverterFactory.create()) + .client(okHttpClient) + .build(); + return retrofit.create(InstancesSocialService.class); + } + + + /** + * Get instance social instances + * + * @return MutableLiveData> + */ + public MutableLiveData getInstances(String search) { + instanceSocialMutableLiveData = new MutableLiveData<>(); + new Thread(() -> { + try { + Call instanceSocialCall = instancesSocialService.getInstances("Bearer " + Helper.INSTANCE_SOCIAL_KEY, search); + Response response = instanceSocialCall.execute(); + if (response.isSuccessful() && response.body() != null) { + Handler mainHandler = new Handler(Looper.getMainLooper()); + Runnable myRunnable = () -> instanceSocialMutableLiveData.setValue(response.body()); + mainHandler.post(myRunnable); + } + } catch (IOException e) { + e.printStackTrace(); + } + }).start(); + return instanceSocialMutableLiveData; + } + +} diff --git a/app/src/main/java/app/fedilab/android/viewmodel/mastodon/InstancesVM.java b/app/src/main/java/app/fedilab/android/viewmodel/mastodon/InstancesVM.java new file mode 100644 index 000000000..af75a11a9 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/viewmodel/mastodon/InstancesVM.java @@ -0,0 +1,150 @@ +package app.fedilab.android.viewmodel.mastodon; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import android.app.Application; +import android.os.Handler; +import android.os.Looper; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import app.fedilab.android.BaseMainActivity; +import app.fedilab.android.client.mastodon.MastodonInstanceService; +import app.fedilab.android.client.mastodon.entities.Emoji; +import app.fedilab.android.client.mastodon.entities.EmojiInstance; +import app.fedilab.android.client.mastodon.entities.Instance; +import app.fedilab.android.client.mastodon.entities.InstanceInfo; +import app.fedilab.android.exception.DBException; +import app.fedilab.android.helper.Helper; +import okhttp3.OkHttpClient; +import retrofit2.Call; +import retrofit2.Response; +import retrofit2.Retrofit; +import retrofit2.converter.gson.GsonConverterFactory; + +public class InstancesVM extends AndroidViewModel { + + + final OkHttpClient okHttpClient = new OkHttpClient.Builder() + .readTimeout(60, TimeUnit.SECONDS) + .connectTimeout(60, TimeUnit.SECONDS) + .proxy(Helper.getProxy(getApplication().getApplicationContext())) + .build(); + private MutableLiveData emojiInstanceMutableLiveData; + private MutableLiveData instanceInfoMutableLiveData; + + public InstancesVM(@NonNull Application application) { + super(application); + } + + private MastodonInstanceService init(@NonNull String instance) { + Retrofit retrofit = new Retrofit.Builder() + .baseUrl("https://" + instance + "/api/v1/") + .addConverterFactory(GsonConverterFactory.create()) + .client(okHttpClient) + .build(); + return retrofit.create(MastodonInstanceService.class); + } + + public LiveData getEmoji(@NonNull String instance) { + MastodonInstanceService mastodonInstanceService = init(instance); + emojiInstanceMutableLiveData = new MutableLiveData<>(); + new Thread(() -> { + EmojiInstance emojiInstance = new EmojiInstance(); + emojiInstance.instance = BaseMainActivity.currentInstance; + Call> emojiCall = mastodonInstanceService.customEmoji(); + if (emojiCall != null) { + try { + Response> emojiResponse = emojiCall.execute(); + if (emojiResponse.isSuccessful()) { + emojiInstance.emojiList = emojiResponse.body(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + if (emojiInstance.emojiList != null) { + try { + new EmojiInstance(getApplication().getApplicationContext()).insertOrUpdate(emojiInstance); + } catch (DBException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + Runnable myRunnable = () -> emojiInstanceMutableLiveData.setValue(emojiInstance); + mainHandler.post(myRunnable); + }).start(); + return emojiInstanceMutableLiveData; + } + + + public LiveData getInstance(@NonNull String instance) { + MastodonInstanceService mastodonInstanceService = init(instance); + instanceInfoMutableLiveData = new MutableLiveData<>(); + new Thread(() -> { + InstanceInfo instanceInfo = new InstanceInfo(); + instanceInfo.instance = BaseMainActivity.currentInstance; + Call instanceCall = mastodonInstanceService.instance(); + if (instanceCall != null) { + try { + Response instanceResponse = instanceCall.execute(); + if (instanceResponse.isSuccessful()) { + instanceInfo.info = instanceResponse.body(); + } + + } catch (IOException e) { + e.printStackTrace(); + } + } + if (instanceInfo.info != null) { + try { + new InstanceInfo(getApplication().getApplicationContext()).insertOrUpdate(instanceInfo); + } catch (DBException e) { + e.printStackTrace(); + } + } else { + instanceInfo.info = new Instance(); + } + //Some values that we must initialize + if (instanceInfo.info.configuration == null) { + instanceInfo.info.configuration = new Instance.Configuration(); + } + if (instanceInfo.info.configuration.pollsConf == null) { + instanceInfo.info.configuration.pollsConf = new Instance.PollsConf(); + } + if (instanceInfo.info.configuration.statusesConf == null) { + instanceInfo.info.configuration.statusesConf = new Instance.StatusesConf(); + } + if (instanceInfo.info.configuration.media_attachments == null) { + instanceInfo.info.configuration.media_attachments = new Instance.MediaConf(); + } + if (instanceInfo.info.rules == null) { + instanceInfo.info.rules = new ArrayList<>(); + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + Runnable myRunnable = () -> instanceInfoMutableLiveData.setValue(instanceInfo); + mainHandler.post(myRunnable); + }).start(); + return instanceInfoMutableLiveData; + } +} diff --git a/app/src/main/java/app/fedilab/android/viewmodel/mastodon/JoinInstancesVM.java b/app/src/main/java/app/fedilab/android/viewmodel/mastodon/JoinInstancesVM.java new file mode 100644 index 000000000..2325b3dfd --- /dev/null +++ b/app/src/main/java/app/fedilab/android/viewmodel/mastodon/JoinInstancesVM.java @@ -0,0 +1,101 @@ +package app.fedilab.android.viewmodel.mastodon; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import android.app.Application; +import android.os.Handler; +import android.os.Looper; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import java.io.IOException; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import app.fedilab.android.client.mastodon.JoinMastodonService; +import app.fedilab.android.client.mastodon.entities.JoinMastodonInstance; +import app.fedilab.android.helper.Helper; +import okhttp3.OkHttpClient; +import retrofit2.Call; +import retrofit2.Response; +import retrofit2.Retrofit; +import retrofit2.converter.gson.GsonConverterFactory; + +public class JoinInstancesVM extends AndroidViewModel { + + final OkHttpClient okHttpClient = new OkHttpClient.Builder() + .readTimeout(60, TimeUnit.SECONDS) + .connectTimeout(60, TimeUnit.SECONDS) + .proxy(Helper.getProxy(getApplication().getApplicationContext())) + .build(); + private final String base_url; + private final JoinMastodonService joinMastodonService; + private MutableLiveData> joiListMutableLiveData; + + + /** + * Constructor - String token can be for the join instances (registration helper) + * + * @param application Application + */ + public JoinInstancesVM(@NonNull Application application) { + super(application); + base_url = "https://api.joinmastodon.org/"; + joinMastodonService = init(); + + } + + private JoinMastodonService init() { + Retrofit retrofit = new Retrofit.Builder() + .baseUrl(base_url) + .addConverterFactory(GsonConverterFactory.create()) + .client(okHttpClient) + .build(); + return retrofit.create(JoinMastodonService.class); + } + + /** + * Find instances through joinmastodon api + * + * @param category String + * @return {@link LiveData} containing a List of {@link JoinMastodonInstance} + */ + public LiveData> getInstances(String category) { + joiListMutableLiveData = new MutableLiveData<>(); + new Thread(() -> { + List joinMastodonInstanceList = null; + Call> listCall = joinMastodonService.getInstances(category); + if (listCall != null) { + try { + Response> listResponse = listCall.execute(); + if (listResponse.isSuccessful()) { + joinMastodonInstanceList = listResponse.body(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + List finalJoinMastodonInstanceList = joinMastodonInstanceList; + Runnable myRunnable = () -> joiListMutableLiveData.setValue(finalJoinMastodonInstanceList); + mainHandler.post(myRunnable); + }).start(); + return joiListMutableLiveData; + } + +} diff --git a/app/src/main/java/app/fedilab/android/viewmodel/mastodon/NodeInfoVM.java b/app/src/main/java/app/fedilab/android/viewmodel/mastodon/NodeInfoVM.java new file mode 100644 index 000000000..ff11bdce0 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/viewmodel/mastodon/NodeInfoVM.java @@ -0,0 +1,106 @@ +package app.fedilab.android.viewmodel.mastodon; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + + +import android.app.Application; +import android.os.Handler; +import android.os.Looper; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +import app.fedilab.android.client.NodeInfoService; +import app.fedilab.android.client.entities.WellKnownNodeinfo; +import app.fedilab.android.helper.Helper; +import okhttp3.OkHttpClient; +import retrofit2.Call; +import retrofit2.Response; +import retrofit2.Retrofit; +import retrofit2.converter.gson.GsonConverterFactory; + +public class NodeInfoVM extends AndroidViewModel { + + final OkHttpClient okHttpClient = new OkHttpClient.Builder() + .readTimeout(5, TimeUnit.SECONDS) + .connectTimeout(5, TimeUnit.SECONDS) + .proxy(Helper.getProxy(getApplication().getApplicationContext())) + .build(); + + private MutableLiveData nodeInfoMutableLiveData; + + + public NodeInfoVM(@NonNull Application application) { + super(application); + } + + private NodeInfoService init(String instance) { + Retrofit retrofit = new Retrofit.Builder() + .baseUrl("https://" + instance + "/") + .addConverterFactory(GsonConverterFactory.create()) + .client(okHttpClient) + .build(); + return retrofit.create(NodeInfoService.class); + } + + + /** + * Get nodeinfo + * + * @return LiveData + */ + public LiveData getNodeInfo(String instance) { + NodeInfoService nodeInfoService = init(instance); + nodeInfoMutableLiveData = new MutableLiveData<>(); + new Thread(() -> { + WellKnownNodeinfo.NodeInfo nodeInfo = null; + + Call nodeInfoLinksCall = nodeInfoService.getWellKnownNodeinfoLinks(); + if (nodeInfoLinksCall != null) { + try { + Response nodeInfoLinksResponse = nodeInfoLinksCall.execute(); + if (nodeInfoLinksResponse.isSuccessful() && nodeInfoLinksResponse.body() != null) { + WellKnownNodeinfo wellKnownNodeinfo = nodeInfoLinksResponse.body(); + Call wellKnownNodeinfoCall = nodeInfoService.getNodeinfo(wellKnownNodeinfo.links.get(0).href); + if (wellKnownNodeinfoCall != null) { + try { + Response response = wellKnownNodeinfoCall.execute(); + if (response.isSuccessful() && response.body() != null) { + nodeInfo = response.body(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + Handler mainHandler = new Handler(Looper.getMainLooper()); + WellKnownNodeinfo.NodeInfo finalNodeInfo = nodeInfo; + Runnable myRunnable = () -> nodeInfoMutableLiveData.setValue(finalNodeInfo); + mainHandler.post(myRunnable); + }).start(); + return nodeInfoMutableLiveData; + } + +} diff --git a/app/src/main/java/app/fedilab/android/viewmodel/mastodon/NotificationsVM.java b/app/src/main/java/app/fedilab/android/viewmodel/mastodon/NotificationsVM.java new file mode 100644 index 000000000..74c633aad --- /dev/null +++ b/app/src/main/java/app/fedilab/android/viewmodel/mastodon/NotificationsVM.java @@ -0,0 +1,364 @@ +package app.fedilab.android.viewmodel.mastodon; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import android.app.Application; +import android.os.Handler; +import android.os.Looper; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import java.io.IOException; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import app.fedilab.android.client.mastodon.MastodonNotificationsService; +import app.fedilab.android.client.mastodon.entities.Notification; +import app.fedilab.android.client.mastodon.entities.Notifications; +import app.fedilab.android.client.mastodon.entities.PushSubscription; +import app.fedilab.android.helper.Helper; +import app.fedilab.android.helper.MastodonHelper; +import app.fedilab.android.helper.SpannableHelper; +import app.fedilab.android.helper.TimelineHelper; +import okhttp3.OkHttpClient; +import retrofit2.Call; +import retrofit2.Response; +import retrofit2.Retrofit; +import retrofit2.converter.gson.GsonConverterFactory; + +public class NotificationsVM extends AndroidViewModel { + + final OkHttpClient okHttpClient = new OkHttpClient.Builder() + .readTimeout(60, TimeUnit.SECONDS) + .connectTimeout(60, TimeUnit.SECONDS) + .proxy(Helper.getProxy(getApplication().getApplicationContext())) + .build(); + + + private MutableLiveData notificationsMutableLiveData; + private MutableLiveData notificationMutableLiveData; + private MutableLiveData voidMutableLiveData; + private MutableLiveData pushSubscriptionMutableLiveData; + + public NotificationsVM(@NonNull Application application) { + super(application); + } + + private MastodonNotificationsService init(@NonNull String instance) { + Retrofit retrofit = new Retrofit.Builder() + .baseUrl("https://" + instance + "/api/v1/") + .addConverterFactory(GsonConverterFactory.create()) + .client(okHttpClient) + .build(); + return retrofit.create(MastodonNotificationsService.class); + } + + /** + * Get notifications for the authenticated account + * + * @param instance String - Instance for the api call + * @param token String - Token of the authenticated account + * @param maxId String - max id for pagination + * @param sinceId String - since id for pagination + * @param minId String - min id for pagination + * @param limit int - result fetched + * @param exlude_types List - type of notifications to exclude in reply + * @param account_id String - target notifications from an account + * @return {@link LiveData} containing a {@link Notifications} + */ + public LiveData getNotifications(@NonNull String instance, String token, + String maxId, + String sinceId, + String minId, + int limit, + List exlude_types, + String account_id) { + notificationsMutableLiveData = new MutableLiveData<>(); + MastodonNotificationsService mastodonNotificationsService = init(instance); + new Thread(() -> { + Notifications notifications = new Notifications(); + Call> notificationsCall = mastodonNotificationsService.getNotifications(token, exlude_types, account_id, maxId, sinceId, minId, limit); + if (notificationsCall != null) { + try { + Response> notificationsResponse = notificationsCall.execute(); + if (notificationsResponse.isSuccessful()) { + List notFilteredNotifications = notificationsResponse.body(); + notifications.notifications = TimelineHelper.filterNotification(getApplication().getApplicationContext(), notFilteredNotifications); + for (Notification notification : notifications.notifications) { + if (notification != null) { + notification.status = SpannableHelper.convertStatus(getApplication().getApplicationContext(), notification.status); + } + } + notifications.pagination = MastodonHelper.getPaginationNotification(notifications.notifications); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + Runnable myRunnable = () -> notificationsMutableLiveData.setValue(notifications); + mainHandler.post(myRunnable); + }).start(); + + return notificationsMutableLiveData; + } + + + /** + * Get a notification for the authenticated account by its id + * + * @param instance String - Instance for the api call + * @param token String - Token of the authenticated account + * @param notification_id String - id of the notification + * @return {@link LiveData} containing a {@link Notification} + */ + public LiveData getSingleNotification(@NonNull String instance, String token, + String notification_id) { + notificationMutableLiveData = new MutableLiveData<>(); + MastodonNotificationsService mastodonNotificationsService = init(instance); + new Thread(() -> { + Notification notification = null; + Call notificationCall = mastodonNotificationsService.getNotification(token, notification_id); + if (notificationCall != null) { + try { + Response notificationResponse = notificationCall.execute(); + if (notificationResponse.isSuccessful()) { + notification = notificationResponse.body(); + if (notification != null) { + notification.status = SpannableHelper.convertStatus(getApplication().getApplicationContext(), notification.status); + } + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + Notification finalNotification = notification; + Runnable myRunnable = () -> notificationMutableLiveData.setValue(finalNotification); + mainHandler.post(myRunnable); + }).start(); + + return notificationMutableLiveData; + } + + /** + * Get a notification for the authenticated account by its id + * + * @param instance String - Instance for the api call + * @param token String - Token of the authenticated account + */ + public LiveData clearNotification(@NonNull String instance, String token) { + voidMutableLiveData = new MutableLiveData<>(); + MastodonNotificationsService mastodonNotificationsService = init(instance); + new Thread(() -> { + Call voidCall = mastodonNotificationsService.clearAllNotifications(token); + if (voidCall != null) { + try { + voidCall.execute(); + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + Runnable myRunnable = () -> voidMutableLiveData.setValue(null); + mainHandler.post(myRunnable); + }).start(); + + return voidMutableLiveData; + } + + + /** + * Get a notification for the authenticated account by its id + * + * @param instance String - Instance for the api call + * @param token String - Token of the authenticated account + * @param notification_id String - id of the notification + */ + public LiveData dismissNotification(@NonNull String instance, String token, String notification_id) { + voidMutableLiveData = new MutableLiveData<>(); + MastodonNotificationsService mastodonNotificationsService = init(instance); + new Thread(() -> { + Call voidCall = mastodonNotificationsService.dismissNotification(token, notification_id); + if (voidCall != null) { + try { + voidCall.execute(); + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + Runnable myRunnable = () -> voidMutableLiveData.setValue(null); + mainHandler.post(myRunnable); + }).start(); + + return voidMutableLiveData; + } + + + /** + * Subscribe to push notifications + * + * @param instance String - server instance + * @param token String + * @param endpoint String - Endpoint URL that is called when a notification event occurs. + * @param keys_p256dh String - User agent public key. Base64 encoded string of public key of ECDH key using prime256v1 curve. + * @param keys_auth String - Auth secret. Base64 encoded string of 16 bytes of random data. + * @param follow boolean - Receive follow notifications? + * @param favourite boolean - Receive favourite notifications? + * @param reblog boolean - Receive reblog notifications? + * @param mention boolean - Receive mention notifications? + * @param poll boolean - Receive poll notifications? + * @return {@link LiveData} containing a {@link PushSubscription} + */ + public LiveData pushSubscription(@NonNull String instance, String token, + String endpoint, + String keys_p256dh, + String keys_auth, + boolean follow, + boolean favourite, + boolean reblog, + boolean mention, + boolean poll) { + pushSubscriptionMutableLiveData = new MutableLiveData<>(); + MastodonNotificationsService mastodonNotificationsService = init(instance); + new Thread(() -> { + PushSubscription pushSubscription = null; + Call pushSubscriptionCall = mastodonNotificationsService.pushSubscription(token, endpoint, keys_p256dh, keys_auth, follow, favourite, reblog, mention, poll); + if (pushSubscriptionCall != null) { + try { + Response pushSubscriptionResponse = pushSubscriptionCall.execute(); + if (pushSubscriptionResponse.isSuccessful()) { + pushSubscription = pushSubscriptionResponse.body(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + PushSubscription finalPushSubscription = pushSubscription; + Runnable myRunnable = () -> pushSubscriptionMutableLiveData.setValue(finalPushSubscription); + mainHandler.post(myRunnable); + }).start(); + + return pushSubscriptionMutableLiveData; + } + + + /** + * Get push notifications + * + * @param instance String - server instance + * @param token String + * @return {@link LiveData} containing a {@link PushSubscription} + */ + public LiveData getPushSubscription(@NonNull String instance, String token) { + pushSubscriptionMutableLiveData = new MutableLiveData<>(); + MastodonNotificationsService mastodonNotificationsService = init(instance); + new Thread(() -> { + PushSubscription pushSubscription = null; + Call pushSubscriptionCall = mastodonNotificationsService.getPushSubscription(token); + if (pushSubscriptionCall != null) { + try { + Response pushSubscriptionResponse = pushSubscriptionCall.execute(); + if (pushSubscriptionResponse.isSuccessful()) { + pushSubscription = pushSubscriptionResponse.body(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + PushSubscription finalPushSubscription = pushSubscription; + Runnable myRunnable = () -> pushSubscriptionMutableLiveData.setValue(finalPushSubscription); + mainHandler.post(myRunnable); + }).start(); + + return pushSubscriptionMutableLiveData; + } + + + /** + * Subscribe to push notifications + * + * @param instance String - server instance + * @param token String + * @param follow boolean - Receive follow notifications? + * @param favourite boolean - Receive favourite notifications? + * @param reblog boolean - Receive reblog notifications? + * @param mention boolean - Receive mention notifications? + * @param poll boolean - Receive poll notifications? + * @return {@link LiveData} containing a {@link PushSubscription} + */ + public LiveData updatePushSubscription(@NonNull String instance, String token, + boolean follow, + boolean favourite, + boolean reblog, + boolean mention, + boolean poll) { + pushSubscriptionMutableLiveData = new MutableLiveData<>(); + MastodonNotificationsService mastodonNotificationsService = init(instance); + new Thread(() -> { + PushSubscription pushSubscription = null; + Call pushSubscriptionCall = mastodonNotificationsService.updatePushSubscription(token, follow, favourite, reblog, mention, poll); + if (pushSubscriptionCall != null) { + try { + Response pushSubscriptionResponse = pushSubscriptionCall.execute(); + if (pushSubscriptionResponse.isSuccessful()) { + pushSubscription = pushSubscriptionResponse.body(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + PushSubscription finalPushSubscription = pushSubscription; + Runnable myRunnable = () -> pushSubscriptionMutableLiveData.setValue(finalPushSubscription); + mainHandler.post(myRunnable); + }).start(); + + return pushSubscriptionMutableLiveData; + } + + /** + * Delete push notifications + * + * @param instance String - Instance for the api call + * @param token String - Token of the authenticated account + */ + public LiveData deletePushsubscription(@NonNull String instance, String token) { + voidMutableLiveData = new MutableLiveData<>(); + MastodonNotificationsService mastodonNotificationsService = init(instance); + new Thread(() -> { + Call voidCall = mastodonNotificationsService.deletePushsubscription(token); + if (voidCall != null) { + try { + voidCall.execute(); + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + Runnable myRunnable = () -> voidMutableLiveData.setValue(null); + mainHandler.post(myRunnable); + }).start(); + + return voidMutableLiveData; + } + +} diff --git a/app/src/main/java/app/fedilab/android/viewmodel/mastodon/OauthVM.java b/app/src/main/java/app/fedilab/android/viewmodel/mastodon/OauthVM.java new file mode 100644 index 000000000..3814e837e --- /dev/null +++ b/app/src/main/java/app/fedilab/android/viewmodel/mastodon/OauthVM.java @@ -0,0 +1,132 @@ +package app.fedilab.android.viewmodel.mastodon; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import android.app.Application; +import android.os.Handler; +import android.os.Looper; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +import app.fedilab.android.client.mastodon.MastodonAppsService; +import app.fedilab.android.client.mastodon.entities.Token; +import app.fedilab.android.helper.Helper; +import okhttp3.OkHttpClient; +import retrofit2.Call; +import retrofit2.Response; +import retrofit2.Retrofit; +import retrofit2.converter.gson.GsonConverterFactory; + +public class OauthVM extends AndroidViewModel { + + final OkHttpClient okHttpClient = new OkHttpClient.Builder() + .readTimeout(60, TimeUnit.SECONDS) + .connectTimeout(60, TimeUnit.SECONDS) + .proxy(Helper.getProxy(getApplication().getApplicationContext())) + .build(); + private MutableLiveData tokenMutableLiveData; + private MutableLiveData voidMutableLiveData; + + + /** + * Constructor - String token can be for the app or the account + * + * @param application Application + */ + public OauthVM(@NonNull Application application) { + super(application); + } + + private MastodonAppsService init(@NonNull String instance) { + Retrofit retrofit = new Retrofit.Builder() + .baseUrl("https://" + instance + "/") + .addConverterFactory(GsonConverterFactory.create()) + .client(okHttpClient) + .build(); + return retrofit.create(MastodonAppsService.class); + } + + + /** + * Obtain a token + * + * @param instance Instance domain of the active account + * @return access token {@link LiveData} containing an {@link Token} + */ + public LiveData createToken(@NonNull String instance, + String grant_type, + String client_id, + String client_secret, + String redirect_uri, + String scope, + String code) { + MastodonAppsService mastodonAppsService = init(instance); + tokenMutableLiveData = new MutableLiveData<>(); + new Thread(() -> { + Token token = null; + Call tokenCall = mastodonAppsService.createToken(grant_type, client_id, client_secret, redirect_uri, scope, code); + if (tokenCall != null) { + try { + Response tokenResponse = tokenCall.execute(); + if (tokenResponse.isSuccessful()) { + token = tokenResponse.body(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + Token finalToken = token; + Runnable myRunnable = () -> tokenMutableLiveData.setValue(finalToken); + mainHandler.post(myRunnable); + }).start(); + return tokenMutableLiveData; + } + + /** + * Delete a token + * + * @param instance Domain of the instance + * @param token Access token to revoke + * @return access token {@link LiveData} containing an {@link Void} + */ + public LiveData revokeToken(@NonNull String instance, + String token, + String client_id, + String client_secret) { + MastodonAppsService mastodonAppsService = init(instance); + voidMutableLiveData = new MutableLiveData<>(); + new Thread(() -> { + Call voidCall = mastodonAppsService.revokeToken(client_id, client_secret, token); + if (voidCall != null) { + try { + voidCall.execute(); + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + Runnable myRunnable = () -> voidMutableLiveData.setValue(null); + mainHandler.post(myRunnable); + }).start(); + return voidMutableLiveData; + } +} diff --git a/app/src/main/java/app/fedilab/android/viewmodel/mastodon/OembedVM.java b/app/src/main/java/app/fedilab/android/viewmodel/mastodon/OembedVM.java new file mode 100644 index 000000000..6ddededb0 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/viewmodel/mastodon/OembedVM.java @@ -0,0 +1,51 @@ +package app.fedilab.android.viewmodel.mastodon; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import android.app.Application; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; + +import java.util.concurrent.TimeUnit; + +import app.fedilab.android.client.mastodon.MastodonOembedService; +import app.fedilab.android.helper.Helper; +import okhttp3.OkHttpClient; +import retrofit2.Retrofit; +import retrofit2.converter.gson.GsonConverterFactory; + +public class OembedVM extends AndroidViewModel { + + final OkHttpClient okHttpClient = new OkHttpClient.Builder() + .readTimeout(60, TimeUnit.SECONDS) + .connectTimeout(60, TimeUnit.SECONDS) + .proxy(Helper.getProxy(getApplication().getApplicationContext())) + .build(); + + public OembedVM(@NonNull Application application) { + super(application); + } + + private MastodonOembedService init(@NonNull String instance) { + Retrofit retrofit = new Retrofit.Builder() + .baseUrl("https://" + instance + "/api/v1/") + .addConverterFactory(GsonConverterFactory.create()) + .client(okHttpClient) + .build(); + return retrofit.create(MastodonOembedService.class); + } + +} diff --git a/app/src/main/java/app/fedilab/android/viewmodel/mastodon/ReorderVM.java b/app/src/main/java/app/fedilab/android/viewmodel/mastodon/ReorderVM.java new file mode 100644 index 000000000..8ae300441 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/viewmodel/mastodon/ReorderVM.java @@ -0,0 +1,147 @@ +package app.fedilab.android.viewmodel.mastodon; +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import android.app.Application; +import android.os.Handler; +import android.os.Looper; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.concurrent.TimeUnit; + +import app.fedilab.android.BaseMainActivity; +import app.fedilab.android.client.entities.Pinned; +import app.fedilab.android.client.mastodon.MastodonSearchService; +import app.fedilab.android.client.mastodon.entities.Results; +import app.fedilab.android.exception.DBException; +import app.fedilab.android.helper.Helper; +import okhttp3.OkHttpClient; +import retrofit2.Call; +import retrofit2.Response; +import retrofit2.Retrofit; +import retrofit2.converter.gson.GsonConverterFactory; + +public class ReorderVM extends AndroidViewModel { + + final OkHttpClient okHttpClient = new OkHttpClient.Builder() + .readTimeout(60, TimeUnit.SECONDS) + .connectTimeout(60, TimeUnit.SECONDS) + .proxy(Helper.getProxy(getApplication().getApplicationContext())) + .build(); + private MutableLiveData resultsMutableLiveData; + private MutableLiveData pinnedMutableLiveData; + + public ReorderVM(@NonNull Application application) { + super(application); + } + + private MastodonSearchService init(@NonNull String url) { + Retrofit retrofit = new Retrofit.Builder() + .baseUrl(url) + .addConverterFactory(GsonConverterFactory.create()) + .client(okHttpClient) + .build(); + return retrofit.create(MastodonSearchService.class); + } + + public LiveData getPinned() { + pinnedMutableLiveData = new MutableLiveData<>(); + new Thread(() -> { + Pinned pinned = null; + try { + pinned = new Pinned(getApplication().getApplicationContext()).getPinned(BaseMainActivity.accountWeakReference.get()); + } catch (DBException e) { + e.printStackTrace(); + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + Pinned finalPinned = pinned; + Runnable myRunnable = () -> pinnedMutableLiveData.setValue(finalPinned); + mainHandler.post(myRunnable); + }).start(); + return pinnedMutableLiveData; + } + + + /** + * Search for content in accounts, statuses and hashtags with API v2 + * + * @param instance Instance domain of the active account + * @param token Access token of the active account + * @param q String - search words + * @param account_id String - If provided, statuses returned will be authored only by this account + * @param type String - Enum(accounts, hashtags, statuses) + * @param exclude_unreviewed boolean - Filter out unreviewed tags? Defaults to false. Use true when trying to find trending tags. + * @param resolve boolean - Attempt WebFinger lookup. Defaults to false. + * @param following boolean - Only include accounts that the user is following. Defaults to false. + * @param offset int - Offset in search results. Used for pagination. Defaults to 0. + * @param max_id String - Return results older than this id + * @param min_id String - Return results immediately newer than this id + * @param limit int - Maximum number of results to load, per type. Defaults to 20. Max 40. + * @return {@link LiveData} containing an {@link Results} + */ + public LiveData search(@NonNull String instance, + String token, + @NonNull String q, + String account_id, + String type, + boolean exclude_unreviewed, + boolean resolve, + boolean following, + int offset, + String max_id, + String min_id, + int limit) { + MastodonSearchService mastodonSearchService = init(instance); + resultsMutableLiveData = new MutableLiveData<>(); + new Thread(() -> { + Call resultsCall = mastodonSearchService.search( + token, q, account_id, type, exclude_unreviewed, + resolve, following, offset, max_id, min_id, limit); + Results results = null; + if (resultsCall != null) { + try { + Response resultsResponse = resultsCall.execute(); + if (resultsResponse.isSuccessful()) { + results = resultsResponse.body(); + if (results != null) { + if (results.statuses == null) { + results.statuses = new ArrayList<>(); + } + if (results.accounts == null) { + results.accounts = new ArrayList<>(); + } + if (results.hashtags == null) { + results.hashtags = new ArrayList<>(); + } + } + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + Results finalResults = results; + Runnable myRunnable = () -> resultsMutableLiveData.setValue(finalResults); + mainHandler.post(myRunnable); + }).start(); + return resultsMutableLiveData; + } +} diff --git a/app/src/main/java/app/fedilab/android/viewmodel/mastodon/SearchVM.java b/app/src/main/java/app/fedilab/android/viewmodel/mastodon/SearchVM.java new file mode 100644 index 000000000..5a0d8078b --- /dev/null +++ b/app/src/main/java/app/fedilab/android/viewmodel/mastodon/SearchVM.java @@ -0,0 +1,153 @@ +package app.fedilab.android.viewmodel.mastodon; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import android.app.Application; +import android.os.Handler; +import android.os.Looper; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import app.fedilab.android.client.entities.StatusCache; +import app.fedilab.android.client.mastodon.MastodonSearchService; +import app.fedilab.android.client.mastodon.entities.Results; +import app.fedilab.android.client.mastodon.entities.Status; +import app.fedilab.android.exception.DBException; +import app.fedilab.android.helper.Helper; +import app.fedilab.android.helper.SpannableHelper; +import okhttp3.OkHttpClient; +import retrofit2.Call; +import retrofit2.Response; +import retrofit2.Retrofit; +import retrofit2.converter.gson.GsonConverterFactory; + +public class SearchVM extends AndroidViewModel { + + final OkHttpClient okHttpClient = new OkHttpClient.Builder() + .readTimeout(60, TimeUnit.SECONDS) + .connectTimeout(60, TimeUnit.SECONDS) + .proxy(Helper.getProxy(getApplication().getApplicationContext())) + .build(); + private MutableLiveData resultsMutableLiveData; + + public SearchVM(@NonNull Application application) { + super(application); + } + + private MastodonSearchService init(@NonNull String instance) { + Retrofit retrofit = new Retrofit.Builder() + .baseUrl("https://" + instance + "/api/v2/") + .addConverterFactory(GsonConverterFactory.create()) + .client(okHttpClient) + .build(); + return retrofit.create(MastodonSearchService.class); + } + + + /** + * Search for content in accounts, statuses and hashtags with API v2 + * + * @param instance Instance domain of the active account + * @param token Access token of the active account + * @param q String - search words + * @param account_id String - If provided, statuses returned will be authored only by this account + * @param type String - Enum(accounts, hashtags, statuses) + * @param exclude_unreviewed boolean - Filter out unreviewed tags? Defaults to false. Use true when trying to find trending tags. + * @param resolve boolean - Attempt WebFinger lookup. Defaults to false. + * @param following boolean - Only include accounts that the user is following. Defaults to false. + * @param offset int - Offset in search results. Used for pagination. Defaults to 0. + * @param max_id String - Return results older than this id + * @param min_id String - Return results immediately newer than this id + * @param limit int - Maximum number of results to load, per type. Defaults to 20. Max 40. + * @return {@link LiveData} containing an {@link Results} + */ + public LiveData search(@NonNull String instance, + String token, + @NonNull String q, + String account_id, + String type, + boolean exclude_unreviewed, + boolean resolve, + boolean following, + int offset, + String max_id, + String min_id, + int limit) { + MastodonSearchService mastodonSearchService = init(instance); + resultsMutableLiveData = new MutableLiveData<>(); + new Thread(() -> { + Call resultsCall = mastodonSearchService.search( + token, q, account_id, type, exclude_unreviewed, + resolve, following, offset, max_id, min_id, limit); + Results results = null; + if (resultsCall != null) { + try { + Response resultsResponse = resultsCall.execute(); + if (resultsResponse.isSuccessful()) { + results = resultsResponse.body(); + if (results != null) { + if (results.statuses == null) { + results.statuses = new ArrayList<>(); + } else { + results.statuses = SpannableHelper.convertStatus(getApplication(), results.statuses); + } + if (results.accounts == null) { + results.accounts = new ArrayList<>(); + } + if (results.hashtags == null) { + results.hashtags = new ArrayList<>(); + } + } + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + Results finalResults = results; + Runnable myRunnable = () -> resultsMutableLiveData.setValue(finalResults); + mainHandler.post(myRunnable); + }).start(); + return resultsMutableLiveData; + } + + public LiveData searchCache(@NonNull String instance, String userId, @NonNull String q) { + resultsMutableLiveData = new MutableLiveData<>(); + new Thread(() -> { + Results results = new Results(); + try { + results.statuses = new ArrayList<>(); + List statuses = new StatusCache(getApplication()).searchStatus(StatusCache.CacheEnum.HOME, instance, userId, q); + statuses = SpannableHelper.convertStatus(getApplication(), statuses); + results.statuses.addAll(statuses); + } catch (DBException e) { + e.printStackTrace(); + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + Runnable myRunnable = () -> resultsMutableLiveData.setValue(results); + mainHandler.post(myRunnable); + }).start(); + return resultsMutableLiveData; + } + +} diff --git a/app/src/main/java/app/fedilab/android/viewmodel/mastodon/StatusesVM.java b/app/src/main/java/app/fedilab/android/viewmodel/mastodon/StatusesVM.java new file mode 100644 index 000000000..e9430677d --- /dev/null +++ b/app/src/main/java/app/fedilab/android/viewmodel/mastodon/StatusesVM.java @@ -0,0 +1,1207 @@ +package app.fedilab.android.viewmodel.mastodon; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + + +import android.app.Application; +import android.net.Uri; +import android.os.Handler; +import android.os.Looper; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import java.io.IOException; +import java.util.Date; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import app.fedilab.android.R; +import app.fedilab.android.client.mastodon.MastodonStatusesService; +import app.fedilab.android.client.mastodon.entities.Account; +import app.fedilab.android.client.mastodon.entities.Accounts; +import app.fedilab.android.client.mastodon.entities.Attachment; +import app.fedilab.android.client.mastodon.entities.Card; +import app.fedilab.android.client.mastodon.entities.Pagination; +import app.fedilab.android.client.mastodon.entities.Poll; +import app.fedilab.android.client.mastodon.entities.ScheduledStatus; +import app.fedilab.android.client.mastodon.entities.ScheduledStatuses; +import app.fedilab.android.client.mastodon.entities.Status; +import app.fedilab.android.helper.Helper; +import app.fedilab.android.helper.MastodonHelper; +import app.fedilab.android.helper.SpannableHelper; +import app.fedilab.android.helper.TimelineHelper; +import okhttp3.Headers; +import okhttp3.MultipartBody; +import okhttp3.OkHttpClient; +import retrofit2.Call; +import retrofit2.Response; +import retrofit2.Retrofit; +import retrofit2.converter.gson.GsonConverterFactory; + + +public class StatusesVM extends AndroidViewModel { + + + private MutableLiveData statusMutableLiveData; + private MutableLiveData scheduledStatusMutableLiveData; + private MutableLiveData scheduledStatusesMutableLiveData; + private MutableLiveData voidMutableLiveData; + private MutableLiveData cardMutableLiveData; + private MutableLiveData attachmentMutableLiveData; + private MutableLiveData pollMutableLiveData; + private MutableLiveData contextMutableLiveData; + private MutableLiveData accountsMutableLiveData; + + + public StatusesVM(@NonNull Application application) { + super(application); + } + + + private OkHttpClient getOkHttpClient() { + return new OkHttpClient.Builder() + .readTimeout(60, TimeUnit.SECONDS) + .connectTimeout(60, TimeUnit.SECONDS) + .proxy(Helper.getProxy(getApplication().getApplicationContext())) + .build(); + } + + private MastodonStatusesService init(@NonNull String instance) { + Retrofit retrofit = new Retrofit.Builder() + .baseUrl("https://" + instance + "/api/v1/") + .addConverterFactory(GsonConverterFactory.create()) + .client(getOkHttpClient()) + .build(); + return retrofit.create(MastodonStatusesService.class); + } + + /** + * Post a media + * + * @param instance Instance domain of the active account + * @param token Access token of the active account + * @param file URI + * @param thumbnail URI + * @param description String + * @param focus String + * @return LiveData + */ + public LiveData postAttachment(@NonNull String instance, + String token, + @NonNull Uri file, + Uri thumbnail, + String description, + String focus) { + MastodonStatusesService mastodonStatusesService = init(instance); + attachmentMutableLiveData = new MutableLiveData<>(); + new Thread(() -> { + MultipartBody.Part fileMultipartBody; + MultipartBody.Part thumbnailMultipartBody; + fileMultipartBody = Helper.getMultipartBody(getApplication(), "file", file); + thumbnailMultipartBody = Helper.getMultipartBody(getApplication(), "file", thumbnail); + Call attachmentCall = mastodonStatusesService.postMedia(token, fileMultipartBody, thumbnailMultipartBody, description, focus); + Attachment attachment = null; + if (attachmentCall != null) { + try { + Response attachmentResponse = attachmentCall.execute(); + if (attachmentResponse.isSuccessful()) { + attachment = attachmentResponse.body(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + Attachment finalAttachment = attachment; + Runnable myRunnable = () -> attachmentMutableLiveData.setValue(finalAttachment); + mainHandler.post(myRunnable); + }).start(); + return attachmentMutableLiveData; + } + + /** + * Post a message with the authenticated user + * text can be null if a media or a poll is attached + * if media are attached, poll need to be null + * if a poll is attached, media should be null + * + * @param instance Instance domain of the active account + * @param token Access token of the active account + * @param idempotency_Key String + * @param text String + * @param media_ids List + * @param poll_options String + * @param poll_expire_in int + * @param poll_multiple boolean + * @param poll_hide_totals boolean + * @param in_reply_to_id String + * @param sensitive boolean + * @param spoiler_text String + * @param visibility String + * @param language String + * @return LiveData + */ + public LiveData postStatus(@NonNull String instance, + String token, + String idempotency_Key, + String text, + List media_ids, + List poll_options, + Integer poll_expire_in, + Boolean poll_multiple, + Boolean poll_hide_totals, + String in_reply_to_id, + Boolean sensitive, + String spoiler_text, + String visibility, + String language) { + MastodonStatusesService mastodonStatusesService = init(instance); + statusMutableLiveData = new MutableLiveData<>(); + new Thread(() -> { + Call statusCall = mastodonStatusesService.createStatus(idempotency_Key, token, text, media_ids, poll_options, poll_expire_in, + poll_multiple, poll_hide_totals, in_reply_to_id, sensitive, spoiler_text, visibility, language); + Status status = null; + if (statusCall != null) { + try { + Response statusResponse = statusCall.execute(); + if (statusResponse.isSuccessful()) { + status = statusResponse.body(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + Status finalStatus = status; + Runnable myRunnable = () -> statusMutableLiveData.setValue(finalStatus); + mainHandler.post(myRunnable); + }).start(); + return statusMutableLiveData; + } + + /** + * @param instance Instance domain of the active account + * @param token Access token of the active account + * Schedule a message for the authenticated user + * scheduledAt can't be null + * text can be null if a media or a poll is attached + * if media are attached, poll need to be null + * if a poll is attached, media should be null + * @param idempotency_Key String + * @param text String + * @param media_ids List + * @param poll_options String + * @param poll_expire_in int + * @param poll_multiple boolean + * @param poll_hide_totals boolean + * @param in_reply_to_id String + * @param sensitive boolean + * @param spoiler_text String + * @param visibility String + * @param scheduledAt Date + * @param language String + * @return LiveData + */ + public LiveData postScheduledStatus(@NonNull String instance, + String token, + String idempotency_Key, + String text, + List media_ids, + List poll_options, + Integer poll_expire_in, + Boolean poll_multiple, + Boolean poll_hide_totals, + String in_reply_to_id, + Boolean sensitive, + String spoiler_text, + String visibility, + @NonNull String scheduledAt, + String language) { + MastodonStatusesService mastodonStatusesService = init(instance); + scheduledStatusMutableLiveData = new MutableLiveData<>(); + new Thread(() -> { + Call scheduledStatusCall = mastodonStatusesService.createScheduledStatus(idempotency_Key, token, text, media_ids, poll_options, poll_expire_in, + poll_multiple, poll_hide_totals, in_reply_to_id, sensitive, spoiler_text, visibility, scheduledAt, language); + ScheduledStatus scheduledStatus = null; + if (scheduledStatusCall != null) { + try { + Response scheduledStatusResponse = scheduledStatusCall.execute(); + if (scheduledStatusResponse.isSuccessful()) { + scheduledStatus = scheduledStatusResponse.body(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + app.fedilab.android.client.mastodon.entities.ScheduledStatus finalScheduledStatus = scheduledStatus; + Runnable myRunnable = () -> scheduledStatusMutableLiveData.setValue(finalScheduledStatus); + mainHandler.post(myRunnable); + }).start(); + return scheduledStatusMutableLiveData; + } + + /** + * Get a status by ID + * + * @param instance Instance domain of the active account + * @param token Access token of the active account + * @param id String - id of the status + * @return LiveData + */ + public LiveData getStatus(@NonNull String instance, String token, String id) { + MastodonStatusesService mastodonStatusesService = init(instance); + statusMutableLiveData = new MutableLiveData<>(); + new Thread(() -> { + Call statusCall = mastodonStatusesService.getStatus(token, id); + Status status = null; + if (statusCall != null) { + try { + Response statusResponse = statusCall.execute(); + if (statusResponse.isSuccessful()) { + status = SpannableHelper.convertStatus(getApplication().getApplicationContext(), statusResponse.body()); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + Status finalStatus = status; + Runnable myRunnable = () -> statusMutableLiveData.setValue(finalStatus); + mainHandler.post(myRunnable); + }).start(); + return statusMutableLiveData; + } + + /** + * Delete a status by ID + * + * @param instance Instance domain of the active account + * @param token Access token of the active account + * @param id String - id of the status + * @return LiveData + */ + public LiveData deleteStatus(@NonNull String instance, String token, String id) { + MastodonStatusesService mastodonStatusesService = init(instance); + statusMutableLiveData = new MutableLiveData<>(); + new Thread(() -> { + Call statusCall = mastodonStatusesService.deleteStatus(token, id); + Status status = null; + if (statusCall != null) { + try { + Response statusResponse = statusCall.execute(); + if (statusResponse.isSuccessful()) { + status = statusResponse.body(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + Status finalStatus = status; + Runnable myRunnable = () -> statusMutableLiveData.setValue(finalStatus); + mainHandler.post(myRunnable); + }).start(); + return statusMutableLiveData; + } + + /** + * Get context of a status + * + * @param instance Instance domain of the active account + * @param token Access token of the active account + * @param id String - id of the status + * @return LiveData + */ + public LiveData getContext(@NonNull String instance, String token, @NonNull String id) { + MastodonStatusesService mastodonStatusesService = init(instance); + contextMutableLiveData = new MutableLiveData<>(); + new Thread(() -> { + Call contextCall = mastodonStatusesService.getContext(token, id); + app.fedilab.android.client.mastodon.entities.Context context = null; + if (contextCall != null) { + try { + Response contextResponse = contextCall.execute(); + if (contextResponse.isSuccessful()) { + context = contextResponse.body(); + if (context != null) { + TimelineHelper.filterStatus(getApplication().getApplicationContext(), context.descendants, TimelineHelper.FilterTimeLineType.CONTEXT); + for (Status status : context.descendants) { + SpannableHelper.convertStatus(getApplication().getApplicationContext(), status); + } + TimelineHelper.filterStatus(getApplication().getApplicationContext(), context.ancestors, TimelineHelper.FilterTimeLineType.CONTEXT); + for (Status status : context.ancestors) { + SpannableHelper.convertStatus(getApplication().getApplicationContext(), status); + } + } + + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + app.fedilab.android.client.mastodon.entities.Context finalContext = context; + Runnable myRunnable = () -> contextMutableLiveData.setValue(finalContext); + mainHandler.post(myRunnable); + }).start(); + return contextMutableLiveData; + } + + /** + * People that reblogged the status by ID + * + * @param instance Instance domain of the active account + * @param token Access token of the active account + * @param id String - id of the status + * @param max_id String + * @param since_id String + * @param min_id String + * @return LiveData + */ + public LiveData rebloggedBy(@NonNull String instance, + String token, + @NonNull String id, + String max_id, + String since_id, + String min_id) { + MastodonStatusesService mastodonStatusesService = init(instance); + accountsMutableLiveData = new MutableLiveData<>(); + new Thread(() -> { + int limit = MastodonHelper.accountsPerCall(getApplication().getApplicationContext()); + Call> accountsCall = mastodonStatusesService.getRebloggedBy(token, id, max_id, since_id, min_id, limit); + List accounts = null; + Headers headers = null; + if (accountsCall != null) { + try { + Response> accountsResponse = accountsCall.execute(); + if (accountsResponse.isSuccessful()) { + accounts = SpannableHelper.convertAccounts(getApplication().getApplicationContext(), accountsResponse.body()); + } + headers = accountsResponse.headers(); + } catch (IOException e) { + e.printStackTrace(); + } + } + Accounts accountsPagination = new Accounts(); + accountsPagination.accounts = accounts; + accountsPagination.pagination = MastodonHelper.getPaginationAccount(accounts); + Handler mainHandler = new Handler(Looper.getMainLooper()); + Runnable myRunnable = () -> accountsMutableLiveData.setValue(accountsPagination); + mainHandler.post(myRunnable); + }).start(); + return accountsMutableLiveData; + } + + /** + * People that favourited the status by ID + * + * @param instance Instance domain of the active account + * @param token Access token of the active account + * @param id String - id of the status + * @param max_id String + * @param since_id String + * @param min_id String + * @return LiveData + */ + public LiveData favouritedBy(@NonNull String instance, + String token, + @NonNull String id, + String max_id, + String since_id, + String min_id) { + MastodonStatusesService mastodonStatusesService = init(instance); + accountsMutableLiveData = new MutableLiveData<>(); + new Thread(() -> { + int limit = MastodonHelper.accountsPerCall(getApplication().getApplicationContext()); + Call> accountsCall = mastodonStatusesService.getFavourited(token, id, max_id, since_id, min_id, limit); + List accounts = null; + Headers headers = null; + if (accountsCall != null) { + try { + Response> accountsResponse = accountsCall.execute(); + if (accountsResponse.isSuccessful()) { + accounts = SpannableHelper.convertAccounts(getApplication().getApplicationContext(), accountsResponse.body()); + } + headers = accountsResponse.headers(); + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + Accounts accountsPagination = new Accounts(); + accountsPagination.accounts = accounts; + accountsPagination.pagination = MastodonHelper.getPaginationAccount(accounts); + Runnable myRunnable = () -> accountsMutableLiveData.setValue(accountsPagination); + mainHandler.post(myRunnable); + }).start(); + return accountsMutableLiveData; + } + + /** + * Add a status to favourites by ID + * + * @param instance Instance domain of the active account + * @param token Access token of the active account + * @param id String - id of the status + * @return LiveData + */ + public LiveData favourite(@NonNull String instance, String token, String id) { + MastodonStatusesService mastodonStatusesService = init(instance); + statusMutableLiveData = new MutableLiveData<>(); + new Thread(() -> { + String errorMessage = null; + Call statusCall = mastodonStatusesService.favourites(token, id); + Status status = null; + if (statusCall != null) { + try { + Response statusResponse = statusCall.execute(); + if (statusResponse.isSuccessful()) { + status = SpannableHelper.convertStatus(getApplication().getApplicationContext(), statusResponse.body()); + } else { + if (statusResponse.errorBody() != null) { + errorMessage = statusResponse.errorBody().string(); + } + } + } catch (IOException e) { + e.printStackTrace(); + errorMessage = e.getMessage() != null ? e.getMessage() : getApplication().getString(R.string.toast_error); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + Status finalStatus = status; + String finalErrorMessage = errorMessage; + Runnable myRunnable = () -> { + statusMutableLiveData.setValue(finalStatus); + if (finalErrorMessage != null) { + Helper.sendToastMessage(getApplication(), Helper.RECEIVE_TOAST_TYPE_ERROR, finalErrorMessage); + } + }; + mainHandler.post(myRunnable); + }).start(); + return statusMutableLiveData; + } + + /** + * remove a status from favourites by ID + * + * @param instance Instance domain of the active account + * @param token Access token of the active account + * @param id String - id of the status + * @return LiveData + */ + public LiveData unFavourite(@NonNull String instance, String token, String id) { + MastodonStatusesService mastodonStatusesService = init(instance); + statusMutableLiveData = new MutableLiveData<>(); + new Thread(() -> { + String errorMessage = null; + Call statusCall = mastodonStatusesService.unFavourite(token, id); + Status status = null; + if (statusCall != null) { + try { + Response statusResponse = statusCall.execute(); + if (statusResponse.isSuccessful()) { + status = SpannableHelper.convertStatus(getApplication().getApplicationContext(), statusResponse.body()); + } else { + if (statusResponse.errorBody() != null) { + errorMessage = statusResponse.errorBody().string(); + } + } + } catch (IOException e) { + e.printStackTrace(); + errorMessage = e.getMessage() != null ? e.getMessage() : getApplication().getString(R.string.toast_error); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + Status finalStatus = status; + String finalErrorMessage = errorMessage; + Runnable myRunnable = () -> { + statusMutableLiveData.setValue(finalStatus); + if (finalErrorMessage != null) { + Helper.sendToastMessage(getApplication(), Helper.RECEIVE_TOAST_TYPE_ERROR, finalErrorMessage); + } + }; + mainHandler.post(myRunnable); + }).start(); + return statusMutableLiveData; + } + + /** + * Reblog a status by ID + * + * @param instance Instance domain of the active account + * @param token Access token of the active account + * @param id String - id of the status + * @param visibility MastodonHelper.visibility - visibility of the reblog (public, unlisted, private) + * @return LiveData + */ + public LiveData reblog(@NonNull String instance, String token, String id, MastodonHelper.visibility visibility) { + MastodonStatusesService mastodonStatusesService = init(instance); + statusMutableLiveData = new MutableLiveData<>(); + + new Thread(() -> { + String errorMessage = null; + Call statusCall = mastodonStatusesService.reblog(token, id, visibility.name().toLowerCase()); + Status status = null; + if (statusCall != null) { + try { + Response statusResponse = statusCall.execute(); + if (statusResponse.isSuccessful()) { + status = SpannableHelper.convertStatus(getApplication().getApplicationContext(), statusResponse.body()); + } else { + if (statusResponse.errorBody() != null) { + errorMessage = statusResponse.errorBody().string(); + } + } + } catch (IOException e) { + e.printStackTrace(); + errorMessage = e.getMessage() != null ? e.getMessage() : getApplication().getString(R.string.toast_error); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + Status finalStatus = status; + String finalErrorMessage = errorMessage; + Runnable myRunnable = () -> { + statusMutableLiveData.setValue(finalStatus); + if (finalErrorMessage != null) { + Helper.sendToastMessage(getApplication(), Helper.RECEIVE_TOAST_TYPE_ERROR, finalErrorMessage); + } + }; + mainHandler.post(myRunnable); + }).start(); + return statusMutableLiveData; + } + + /** + * Unreblog a status by ID + * + * @param instance Instance domain of the active account + * @param token Access token of the active account + * @param id String - id of the status + * @return LiveData + */ + public LiveData unReblog(@NonNull String instance, String token, String id) { + MastodonStatusesService mastodonStatusesService = init(instance); + statusMutableLiveData = new MutableLiveData<>(); + new Thread(() -> { + String errorMessage = null; + Call statusCall = mastodonStatusesService.unReblog(token, id); + Status status = null; + if (statusCall != null) { + try { + Response statusResponse = statusCall.execute(); + if (statusResponse.isSuccessful()) { + status = SpannableHelper.convertStatus(getApplication().getApplicationContext(), statusResponse.body()); + } else { + if (statusResponse.errorBody() != null) { + errorMessage = statusResponse.errorBody().string(); + } + } + } catch (IOException e) { + e.printStackTrace(); + errorMessage = e.getMessage() != null ? e.getMessage() : getApplication().getString(R.string.toast_error); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + Status finalStatus = status; + String finalErrorMessage = errorMessage; + Runnable myRunnable = () -> { + statusMutableLiveData.setValue(finalStatus); + if (finalErrorMessage != null) { + Helper.sendToastMessage(getApplication(), Helper.RECEIVE_TOAST_TYPE_ERROR, finalErrorMessage); + } + }; + mainHandler.post(myRunnable); + }).start(); + return statusMutableLiveData; + } + + /** + * Bookmark a status by ID + * + * @param instance Instance domain of the active account + * @param token Access token of the active account + * @param id String - id of the status + * @return LiveData + */ + public LiveData bookmark(@NonNull String instance, String token, String id) { + MastodonStatusesService mastodonStatusesService = init(instance); + statusMutableLiveData = new MutableLiveData<>(); + new Thread(() -> { + String errorMessage = null; + Call statusCall = mastodonStatusesService.bookmark(token, id); + Status status = null; + if (statusCall != null) { + try { + Response statusResponse = statusCall.execute(); + if (statusResponse.isSuccessful()) { + status = SpannableHelper.convertStatus(getApplication().getApplicationContext(), statusResponse.body()); + } else { + if (statusResponse.errorBody() != null) { + errorMessage = statusResponse.errorBody().string(); + } + } + } catch (IOException e) { + e.printStackTrace(); + errorMessage = e.getMessage() != null ? e.getMessage() : getApplication().getString(R.string.toast_error); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + Status finalStatus = status; + String finalErrorMessage = errorMessage; + Runnable myRunnable = () -> { + statusMutableLiveData.setValue(finalStatus); + if (finalErrorMessage != null) { + Helper.sendToastMessage(getApplication(), Helper.RECEIVE_TOAST_TYPE_ERROR, finalErrorMessage); + } + }; + mainHandler.post(myRunnable); + }).start(); + return statusMutableLiveData; + } + + /** + * Unbookmark a status by ID + * + * @param instance Instance domain of the active account + * @param token Access token of the active account + * @param id String - id of the status + * @return LiveData + */ + public LiveData unBookmark(@NonNull String instance, String token, String id) { + MastodonStatusesService mastodonStatusesService = init(instance); + statusMutableLiveData = new MutableLiveData<>(); + new Thread(() -> { + String errorMessage = null; + Call statusCall = mastodonStatusesService.unBookmark(token, id); + Status status = null; + if (statusCall != null) { + try { + Response statusResponse = statusCall.execute(); + if (statusResponse.isSuccessful()) { + status = SpannableHelper.convertStatus(getApplication().getApplicationContext(), statusResponse.body()); + } else { + if (statusResponse.errorBody() != null) { + errorMessage = statusResponse.errorBody().string(); + } + } + } catch (IOException e) { + e.printStackTrace(); + errorMessage = e.getMessage() != null ? e.getMessage() : getApplication().getString(R.string.toast_error); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + Status finalStatus = status; + String finalErrorMessage = errorMessage; + Runnable myRunnable = () -> { + statusMutableLiveData.setValue(finalStatus); + if (finalErrorMessage != null) { + Helper.sendToastMessage(getApplication(), Helper.RECEIVE_TOAST_TYPE_ERROR, finalErrorMessage); + } + }; + mainHandler.post(myRunnable); + }).start(); + return statusMutableLiveData; + } + + /** + * Mute a conversation by status ID + * + * @param instance Instance domain of the active account + * @param token Access token of the active account + * @param id String - id of the status + * @return LiveData + */ + public LiveData mute(@NonNull String instance, String token, String id) { + MastodonStatusesService mastodonStatusesService = init(instance); + statusMutableLiveData = new MutableLiveData<>(); + new Thread(() -> { + String errorMessage = null; + Call statusCall = mastodonStatusesService.muteConversation(token, id); + Status status = null; + if (statusCall != null) { + try { + Response statusResponse = statusCall.execute(); + if (statusResponse.isSuccessful()) { + status = SpannableHelper.convertStatus(getApplication().getApplicationContext(), statusResponse.body()); + } else { + if (statusResponse.errorBody() != null) { + errorMessage = statusResponse.errorBody().string(); + } + } + } catch (IOException e) { + e.printStackTrace(); + errorMessage = e.getMessage() != null ? e.getMessage() : getApplication().getString(R.string.toast_error); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + Status finalStatus = status; + String finalErrorMessage = errorMessage; + Runnable myRunnable = () -> { + statusMutableLiveData.setValue(finalStatus); + if (finalErrorMessage != null) { + Helper.sendToastMessage(getApplication(), Helper.RECEIVE_TOAST_TYPE_ERROR, finalErrorMessage); + } + }; + mainHandler.post(myRunnable); + }).start(); + return statusMutableLiveData; + } + + /** + * Unmute a conversation by a status ID + * + * @param instance Instance domain of the active account + * @param token Access token of the active account + * @param id String - id of the status + * @return LiveData + */ + public LiveData unMute(@NonNull String instance, String token, String id) { + MastodonStatusesService mastodonStatusesService = init(instance); + statusMutableLiveData = new MutableLiveData<>(); + new Thread(() -> { + String errorMessage = null; + Call statusCall = mastodonStatusesService.unMuteConversation(token, id); + Status status = null; + if (statusCall != null) { + try { + Response statusResponse = statusCall.execute(); + if (statusResponse.isSuccessful()) { + status = SpannableHelper.convertStatus(getApplication().getApplicationContext(), statusResponse.body()); + } else { + if (statusResponse.errorBody() != null) { + errorMessage = statusResponse.errorBody().string(); + } + } + } catch (IOException e) { + e.printStackTrace(); + errorMessage = e.getMessage() != null ? e.getMessage() : getApplication().getString(R.string.toast_error); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + Status finalStatus = status; + String finalErrorMessage = errorMessage; + Runnable myRunnable = () -> { + statusMutableLiveData.setValue(finalStatus); + if (finalErrorMessage != null) { + Helper.sendToastMessage(getApplication(), Helper.RECEIVE_TOAST_TYPE_ERROR, finalErrorMessage); + } + }; + mainHandler.post(myRunnable); + }).start(); + return statusMutableLiveData; + } + + /** + * Pin a status by ID + * + * @param instance Instance domain of the active account + * @param token Access token of the active account + * @param id String - id of the status + * @return LiveData + */ + public LiveData pin(@NonNull String instance, String token, String id) { + MastodonStatusesService mastodonStatusesService = init(instance); + statusMutableLiveData = new MutableLiveData<>(); + new Thread(() -> { + Call statusCall = mastodonStatusesService.pin(token, id); + String errorMessage = null; + Status status = null; + if (statusCall != null) { + try { + Response statusResponse = statusCall.execute(); + if (statusResponse.isSuccessful()) { + status = SpannableHelper.convertStatus(getApplication().getApplicationContext(), statusResponse.body()); + } else { + if (statusResponse.errorBody() != null) { + errorMessage = statusResponse.errorBody().string(); + } + } + } catch (IOException e) { + e.printStackTrace(); + errorMessage = e.getMessage() != null ? e.getMessage() : getApplication().getString(R.string.toast_error); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + Status finalStatus = status; + String finalErrorMessage = errorMessage; + Runnable myRunnable = () -> { + statusMutableLiveData.setValue(finalStatus); + if (finalErrorMessage != null) { + Helper.sendToastMessage(getApplication(), Helper.RECEIVE_TOAST_TYPE_ERROR, finalErrorMessage); + } + }; + mainHandler.post(myRunnable); + }).start(); + return statusMutableLiveData; + } + + /** + * Unpin a status by ID + * + * @param instance Instance domain of the active account + * @param token Access token of the active account + * @param id String - id of the status + * @return LiveData + */ + public LiveData unPin(@NonNull String instance, String token, String id) { + MastodonStatusesService mastodonStatusesService = init(instance); + statusMutableLiveData = new MutableLiveData<>(); + new Thread(() -> { + String errorMessage = null; + Call statusCall = mastodonStatusesService.unPin(token, id); + Status status = null; + if (statusCall != null) { + try { + Response statusResponse = statusCall.execute(); + if (statusResponse.isSuccessful()) { + status = SpannableHelper.convertStatus(getApplication().getApplicationContext(), statusResponse.body()); + } else { + if (statusResponse.errorBody() != null) { + errorMessage = statusResponse.errorBody().string(); + } + } + } catch (IOException e) { + e.printStackTrace(); + errorMessage = e.getMessage() != null ? e.getMessage() : getApplication().getString(R.string.toast_error); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + Status finalStatus = status; + String finalErrorMessage = errorMessage; + Runnable myRunnable = () -> { + statusMutableLiveData.setValue(finalStatus); + if (finalErrorMessage != null) { + Helper.sendToastMessage(getApplication(), Helper.RECEIVE_TOAST_TYPE_ERROR, finalErrorMessage); + } + }; + mainHandler.post(myRunnable); + }).start(); + return statusMutableLiveData; + } + + /** + * Get card + * + * @param instance Instance domain of the active account + * @param token Access token of the active account + * @param id String - id of the status + * @return LiveData + */ + public LiveData getCard(@NonNull String instance, String token, String id) { + MastodonStatusesService mastodonStatusesService = init(instance); + cardMutableLiveData = new MutableLiveData<>(); + new Thread(() -> { + Call cardCall = mastodonStatusesService.getCard(token, id); + Card card = null; + if (cardCall != null) { + try { + Response cardResponse = cardCall.execute(); + if (cardResponse.isSuccessful()) { + card = cardResponse.body(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + Card finalCard = card; + Runnable myRunnable = () -> cardMutableLiveData.setValue(finalCard); + mainHandler.post(myRunnable); + }).start(); + return cardMutableLiveData; + } + + /** + * Get attachment + * + * @param instance Instance domain of the active account + * @param token Access token of the active account + * @param id String - id of the status + * @return LiveData + */ + public LiveData getAttachment(@NonNull String instance, String token, String id) { + MastodonStatusesService mastodonStatusesService = init(instance); + attachmentMutableLiveData = new MutableLiveData<>(); + new Thread(() -> { + Call attachmentCall = mastodonStatusesService.getMedia(token, id); + Attachment attachment = null; + if (attachmentCall != null) { + try { + Response attachmentResponse = attachmentCall.execute(); + if (attachmentResponse.isSuccessful()) { + attachment = attachmentResponse.body(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + Attachment finalAttachment = attachment; + Runnable myRunnable = () -> attachmentMutableLiveData.setValue(finalAttachment); + mainHandler.post(myRunnable); + }).start(); + return attachmentMutableLiveData; + } + + /** + * Update a media + * + * @param instance Instance domain of the active account + * @param token Access token of the active account + * @param id String - Id of the media to update + * @param file URI + * @param thumbnail URI + * @param description String + * @param focus String + * @return LiveData + */ + public LiveData updateAttachment(@NonNull String instance, + String token, + @NonNull String id, + @NonNull Uri file, + Uri thumbnail, + String description, + String focus) { + MastodonStatusesService mastodonStatusesService = init(instance); + attachmentMutableLiveData = new MutableLiveData<>(); + new Thread(() -> { + MultipartBody.Part fileMultipartBody = null; + MultipartBody.Part thumbnailMultipartBody = null; + fileMultipartBody = Helper.getMultipartBody(getApplication(), "file", file); + thumbnailMultipartBody = Helper.getMultipartBody(getApplication(), "file", thumbnail); + Call attachmentCall = mastodonStatusesService.updateMedia(token, id, fileMultipartBody, thumbnailMultipartBody, description, focus); + Attachment attachment = null; + if (attachmentCall != null) { + try { + Response attachmentResponse = attachmentCall.execute(); + if (attachmentResponse.isSuccessful()) { + attachment = attachmentResponse.body(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + Attachment finalAttachment = attachment; + Runnable myRunnable = () -> attachmentMutableLiveData.setValue(finalAttachment); + mainHandler.post(myRunnable); + }).start(); + return attachmentMutableLiveData; + } + + /** + * Get Poll + * + * @param instance Instance domain of the active account + * @param token Access token of the active account + * @param id String - id of the status + * @return LiveData + */ + public LiveData getPoll(@NonNull String instance, String token, String id) { + MastodonStatusesService mastodonStatusesService = init(instance); + pollMutableLiveData = new MutableLiveData<>(); + new Thread(() -> { + Call pollCall = mastodonStatusesService.getPoll(token, id); + Poll poll = null; + if (pollCall != null) { + try { + Response pollResponse = pollCall.execute(); + if (pollResponse.isSuccessful()) { + poll = pollResponse.body(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + Poll finalPoll = poll; + Runnable myRunnable = () -> pollMutableLiveData.setValue(finalPoll); + mainHandler.post(myRunnable); + }).start(); + return pollMutableLiveData; + } + + /** + * Vote on a Poll + * + * @param instance Instance domain of the active account + * @param token Access token of the active account + * @param id String - id of the poll + * @param choices int[] - array of choices + * @return LiveData + */ + public LiveData votePoll(@NonNull String instance, String token, String id, int[] choices) { + MastodonStatusesService mastodonStatusesService = init(instance); + pollMutableLiveData = new MutableLiveData<>(); + new Thread(() -> { + Call pollCall = mastodonStatusesService.votePoll(token, id, choices); + Poll poll = null; + if (pollCall != null) { + try { + Response pollResponse = pollCall.execute(); + if (pollResponse.isSuccessful()) { + poll = pollResponse.body(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + Poll finalPoll = poll; + Runnable myRunnable = () -> pollMutableLiveData.setValue(finalPoll); + mainHandler.post(myRunnable); + }).start(); + return pollMutableLiveData; + } + + /** + * Get list of scheduled status + * + * @param instance Instance domain of the active account + * @param token Access token of the active account + * @param max_id String + * @param since_id String + * @param min_id String + * @param limit int + * @return LiveData + */ + public LiveData getScheduledStatuses(@NonNull String instance, + String token, + String max_id, + String since_id, + String min_id, + int limit) { + MastodonStatusesService mastodonStatusesService = init(instance); + scheduledStatusesMutableLiveData = new MutableLiveData<>(); + new Thread(() -> { + Call> scheduledStatuseCall = mastodonStatusesService.getScheduledStatuses(token, max_id, since_id, min_id, limit); + List scheduledStatusList = null; + Pagination pagination = null; + if (scheduledStatuseCall != null) { + try { + Response> scheduledStatusResponse = scheduledStatuseCall.execute(); + if (scheduledStatusResponse.isSuccessful()) { + scheduledStatusList = scheduledStatusResponse.body(); + pagination = MastodonHelper.getPaginationScheduledStatus(scheduledStatusList); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + ScheduledStatuses scheduledStatuses = new ScheduledStatuses(); + scheduledStatuses.scheduledStatuses = scheduledStatusList; + scheduledStatuses.pagination = pagination; + Handler mainHandler = new Handler(Looper.getMainLooper()); + Runnable myRunnable = () -> scheduledStatusesMutableLiveData.setValue(scheduledStatuses); + mainHandler.post(myRunnable); + }).start(); + return scheduledStatusesMutableLiveData; + } + + /** + * Get a scheduled status + * + * @param instance Instance domain of the active account + * @param token Access token of the active account + * @param id String - id of the scheduled status + * @return LiveData + */ + public LiveData getScheduledStatus(@NonNull String instance, String token, String id) { + MastodonStatusesService mastodonStatusesService = init(instance); + scheduledStatusMutableLiveData = new MutableLiveData<>(); + new Thread(() -> { + Call scheduledStatusCall = mastodonStatusesService.getScheduledStatus(token, id); + ScheduledStatus scheduledStatus = null; + if (scheduledStatusCall != null) { + try { + Response scheduledStatusResponse = scheduledStatusCall.execute(); + if (scheduledStatusResponse.isSuccessful()) { + scheduledStatus = scheduledStatusResponse.body(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + ScheduledStatus finalScheduledStatus = scheduledStatus; + Runnable myRunnable = () -> scheduledStatusMutableLiveData.setValue(finalScheduledStatus); + mainHandler.post(myRunnable); + }).start(); + return scheduledStatusMutableLiveData; + } + + /** + * Update a scheduled status + * + * @param instance Instance domain of the active account + * @param token Access token of the active account + * @param id String - id of the scheduled status + * @return LiveData + */ + public LiveData updateScheduledStatus(@NonNull String instance, String token, String id, Date scheduled_at) { + MastodonStatusesService mastodonStatusesService = init(instance); + scheduledStatusMutableLiveData = new MutableLiveData<>(); + new Thread(() -> { + Call scheduledStatusCall = mastodonStatusesService.updateScheduleStatus(token, id, scheduled_at); + ScheduledStatus scheduledStatus = null; + if (scheduledStatusCall != null) { + try { + Response scheduledStatusResponse = scheduledStatusCall.execute(); + if (scheduledStatusResponse.isSuccessful()) { + scheduledStatus = scheduledStatusResponse.body(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + ScheduledStatus finalScheduledStatus = scheduledStatus; + Runnable myRunnable = () -> scheduledStatusMutableLiveData.setValue(finalScheduledStatus); + mainHandler.post(myRunnable); + }).start(); + return scheduledStatusMutableLiveData; + } + + /** + * Delete a scheduled status + * + * @param instance Instance domain of the active account + * @param token Access token of the active account + * @param id String - id of the scheduled status + * @return LiveData + */ + public LiveData deleteScheduledStatus(@NonNull String instance, String token, String id) { + MastodonStatusesService mastodonStatusesService = init(instance); + voidMutableLiveData = new MutableLiveData<>(); + new Thread(() -> { + Call voidCall = mastodonStatusesService.deleteScheduledStatus(token, id); + if (voidCall != null) { + try { + voidCall.execute(); + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + Runnable myRunnable = () -> voidMutableLiveData.setValue(null); + mainHandler.post(myRunnable); + }).start(); + return voidMutableLiveData; + } +} diff --git a/app/src/main/java/app/fedilab/android/viewmodel/mastodon/TimelinesVM.java b/app/src/main/java/app/fedilab/android/viewmodel/mastodon/TimelinesVM.java new file mode 100644 index 000000000..b9cfb17f7 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/viewmodel/mastodon/TimelinesVM.java @@ -0,0 +1,684 @@ +package app.fedilab.android.viewmodel.mastodon; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + + +import android.app.Application; +import android.os.Handler; +import android.os.Looper; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import java.io.IOException; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import app.fedilab.android.BaseMainActivity; +import app.fedilab.android.client.entities.StatusCache; +import app.fedilab.android.client.entities.StatusDraft; +import app.fedilab.android.client.mastodon.MastodonTimelinesService; +import app.fedilab.android.client.mastodon.entities.Account; +import app.fedilab.android.client.mastodon.entities.Conversation; +import app.fedilab.android.client.mastodon.entities.Conversations; +import app.fedilab.android.client.mastodon.entities.Marker; +import app.fedilab.android.client.mastodon.entities.MastodonList; +import app.fedilab.android.client.mastodon.entities.Status; +import app.fedilab.android.client.mastodon.entities.Statuses; +import app.fedilab.android.exception.DBException; +import app.fedilab.android.helper.Helper; +import app.fedilab.android.helper.MastodonHelper; +import app.fedilab.android.helper.SpannableHelper; +import app.fedilab.android.helper.TimelineHelper; +import okhttp3.OkHttpClient; +import retrofit2.Call; +import retrofit2.Response; +import retrofit2.Retrofit; +import retrofit2.converter.gson.GsonConverterFactory; + +public class TimelinesVM extends AndroidViewModel { + + final OkHttpClient okHttpClient = new OkHttpClient.Builder() + .readTimeout(60, TimeUnit.SECONDS) + .connectTimeout(60, TimeUnit.SECONDS) + .proxy(Helper.getProxy(getApplication().getApplicationContext())) + .build(); + + + private MutableLiveData> accountListMutableLiveData; + private MutableLiveData> statusDraftListMutableLiveData; + private MutableLiveData statusMutableLiveData; + private MutableLiveData statusesMutableLiveData; + private MutableLiveData conversationListMutableLiveData; + private MutableLiveData mastodonListMutableLiveData; + private MutableLiveData> mastodonListListMutableLiveData; + private MutableLiveData markerMutableLiveData; + + public TimelinesVM(@NonNull Application application) { + super(application); + } + + + private MastodonTimelinesService init(String instance) { + Retrofit retrofit = new Retrofit.Builder() + .baseUrl("https://" + instance + "/api/v1/") + .addConverterFactory(GsonConverterFactory.create()) + .client(okHttpClient) + .build(); + return retrofit.create(MastodonTimelinesService.class); + } + + /** + * Public timeline + * + * @param local Show only local statuses? Defaults to false. + * @param remote Show only remote statuses? Defaults to false. + * @param onlyMedia Show only statuses with media attached? Defaults to false. + * @param maxId Return results older than this id + * @param sinceId Return results newer than this id + * @param minId Return results immediately newer than this id + * @param limit Maximum number of results to return. Defaults to 20. + * @return {@link LiveData} containing a {@link Statuses} + */ + public LiveData getPublic(String token, @NonNull String instance, + boolean local, + boolean remote, + boolean onlyMedia, + String maxId, + String sinceId, + String minId, + int limit) { + MastodonTimelinesService mastodonTimelinesService = init(instance); + statusesMutableLiveData = new MutableLiveData<>(); + new Thread(() -> { + Call> publicTlCall = mastodonTimelinesService.getPublic(token, local, remote, onlyMedia, maxId, sinceId, minId, limit); + Statuses statuses = new Statuses(); + if (publicTlCall != null) { + try { + Response> publicTlResponse = publicTlCall.execute(); + if (publicTlResponse.isSuccessful()) { + List notFilteredStatuses = publicTlResponse.body(); + List filteredStatuses = TimelineHelper.filterStatus(getApplication(), notFilteredStatuses, TimelineHelper.FilterTimeLineType.PUBLIC); + statuses.statuses = SpannableHelper.convertStatus(getApplication().getApplicationContext(), filteredStatuses); + statuses.pagination = MastodonHelper.getPaginationStatus(statuses.statuses); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + Runnable myRunnable = () -> statusesMutableLiveData.setValue(statuses); + mainHandler.post(myRunnable); + }).start(); + return statusesMutableLiveData; + } + + + /** + * View public statuses containing the given hashtag. + * + * @param hashtag Content of a #hashtag, not including # symbol. + * @param local If true, return only local statuses. Defaults to false. + * @param onlyMedia If true, return only statuses with media attachments. Defaults to false. + * @param maxId Return results older than this ID. + * @param sinceId Return results newer than this ID. + * @param minId Return results immediately newer than this ID. + * @param limit Maximum number of results to return. Defaults to 20. + * @return {@link LiveData} containing a {@link Statuses} + */ + public LiveData getHashTag(String token, @NonNull String instance, + @NonNull String hashtag, + boolean local, + boolean onlyMedia, + List all, + List any, + List none, + String maxId, + String sinceId, + String minId, + int limit) { + statusesMutableLiveData = new MutableLiveData<>(); + MastodonTimelinesService mastodonTimelinesService = init(instance); + new Thread(() -> { + Statuses statuses = new Statuses(); + Call> hashTagTlCall = mastodonTimelinesService.getHashTag(token, hashtag, local, onlyMedia, all, any, none, maxId, sinceId, minId, limit); + if (hashTagTlCall != null) { + try { + Response> hashTagTlResponse = hashTagTlCall.execute(); + if (hashTagTlResponse.isSuccessful()) { + List notFilteredStatuses = hashTagTlResponse.body(); + List filteredStatuses = TimelineHelper.filterStatus(getApplication().getApplicationContext(), notFilteredStatuses, TimelineHelper.FilterTimeLineType.PUBLIC); + statuses.statuses = SpannableHelper.convertStatus(getApplication().getApplicationContext(), filteredStatuses); + statuses.pagination = MastodonHelper.getPaginationStatus(statuses.statuses); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + Runnable myRunnable = () -> statusesMutableLiveData.setValue(statuses); + mainHandler.post(myRunnable); + }).start(); + + return statusesMutableLiveData; + } + + /** + * View statuses from followed users. + * + * @param maxId Return results older than id + * @param sinceId Return results newer than id + * @param minId Return results immediately newer than id + * @param limit Maximum number of results to return. Defaults to 20. + * @param local Return only local statuses? + * @return {@link LiveData} containing a {@link Statuses} + */ + public LiveData getHome(@NonNull String instance, String token, + String maxId, + String sinceId, + String minId, + int limit, + boolean local) { + statusesMutableLiveData = new MutableLiveData<>(); + MastodonTimelinesService mastodonTimelinesService = init(instance); + new Thread(() -> { + Statuses statuses = new Statuses(); + Call> homeTlCall = mastodonTimelinesService.getHome(token, maxId, sinceId, minId, limit, local); + if (homeTlCall != null) { + try { + Response> homeTlResponse = homeTlCall.execute(); + if (homeTlResponse.isSuccessful()) { + List notFilteredStatuses = homeTlResponse.body(); + List filteredStatuses = TimelineHelper.filterStatus(getApplication().getApplicationContext(), notFilteredStatuses, TimelineHelper.FilterTimeLineType.HOME); + statuses.statuses = SpannableHelper.convertStatus(getApplication().getApplicationContext(), filteredStatuses); + statuses.pagination = MastodonHelper.getPaginationStatus(statuses.statuses); + for (Status status : statuses.statuses) { + StatusCache statusCacheDAO = new StatusCache(getApplication().getApplicationContext()); + StatusCache statusCache = new StatusCache(); + statusCache.instance = instance; + statusCache.user_id = BaseMainActivity.currentUserID; + statusCache.status = status; + statusCache.type = StatusCache.CacheEnum.HOME; + statusCache.status_id = status.id; + try { + statusCacheDAO.insertOrUpdate(statusCache); + } catch (DBException e) { + e.printStackTrace(); + } + } + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + Runnable myRunnable = () -> statusesMutableLiveData.setValue(statuses); + mainHandler.post(myRunnable); + }).start(); + + return statusesMutableLiveData; + } + + /** + * Get home status from cache + * + * @param instance String - instance + * @param user_id String - user id + * @param maxId String - max id + * @param minId String - min id + * @return LiveData + */ + public LiveData getHomeCache(@NonNull String instance, String user_id, + String maxId, + String minId) { + statusesMutableLiveData = new MutableLiveData<>(); + new Thread(() -> { + StatusCache statusCacheDAO = new StatusCache(getApplication().getApplicationContext()); + Statuses statuses = null; + try { + statuses = statusCacheDAO.geStatuses(StatusCache.CacheEnum.HOME, instance, user_id, maxId, minId); + if (statuses != null) { + statuses.statuses = SpannableHelper.convertStatus(getApplication().getApplicationContext(), statuses.statuses); + } + } catch (DBException e) { + e.printStackTrace(); + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + Statuses finalStatuses = statuses; + Runnable myRunnable = () -> statusesMutableLiveData.setValue(finalStatuses); + mainHandler.post(myRunnable); + }).start(); + return statusesMutableLiveData; + } + + + /** + * Get user drafts + * + * @param account app.fedilab.android.client.entities.Account + * @return LiveData> + */ + public LiveData> getDrafts(app.fedilab.android.client.entities.Account account) { + statusDraftListMutableLiveData = new MutableLiveData<>(); + new Thread(() -> { + List statusCacheDAO = null; + try { + statusCacheDAO = new StatusDraft(getApplication().getApplicationContext()).geStatusDraftList(account); + } catch (DBException e) { + e.printStackTrace(); + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + List finalStatusCacheDAO = statusCacheDAO; + Runnable myRunnable = () -> statusDraftListMutableLiveData.setValue(finalStatusCacheDAO); + mainHandler.post(myRunnable); + }).start(); + return statusDraftListMutableLiveData; + } + + /** + * View statuses in the given list timeline. + * + * @param listId Local ID of the list in the database. + * @param maxId Return results older than this ID. + * @param sinceId Return results newer than this ID. + * @param minId Return results immediately newer than this ID. + * @param limit Maximum number of results to return. Defaults to 20.Return results older than this ID. + * @return {@link LiveData} containing a {@link Statuses} + */ + public LiveData getList(@NonNull String instance, String token, + @NonNull String listId, + String maxId, + String sinceId, + String minId, + int limit) { + statusesMutableLiveData = new MutableLiveData<>(); + MastodonTimelinesService mastodonTimelinesService = init(instance); + new Thread(() -> { + Statuses statuses = new Statuses(); + Call> listTlCall = mastodonTimelinesService.getList(token, listId, maxId, sinceId, minId, limit); + if (listTlCall != null) { + try { + Response> listTlResponse = listTlCall.execute(); + if (listTlResponse.isSuccessful()) { + statuses.statuses = SpannableHelper.convertStatus(getApplication().getApplicationContext(), listTlResponse.body()); + statuses.pagination = MastodonHelper.getPaginationStatus(statuses.statuses); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + Runnable myRunnable = () -> statusesMutableLiveData.setValue(statuses); + mainHandler.post(myRunnable); + }).start(); + + return statusesMutableLiveData; + } + + /** + * Show conversations + * + * @return {@link LiveData} containing a {@link Conversations} + */ + public LiveData getConversations(@NonNull String instance, String token, + String maxId, + String sinceId, + String minId, + int limit) { + conversationListMutableLiveData = new MutableLiveData<>(); + MastodonTimelinesService mastodonTimelinesService = init(instance); + new Thread(() -> { + Conversations conversations = null; + Call> conversationsCall = mastodonTimelinesService.getConversations(token, maxId, sinceId, minId, limit); + if (conversationsCall != null) { + conversations = new Conversations(); + try { + Response> conversationsResponse = conversationsCall.execute(); + if (conversationsResponse.isSuccessful()) { + conversations.conversations = conversationsResponse.body(); + if (conversations.conversations != null) { + for (Conversation conversation : conversations.conversations) { + conversation.last_status = SpannableHelper.convertStatus(getApplication().getApplicationContext(), conversation.last_status); + } + } + conversations.pagination = MastodonHelper.getPaginationConversation(conversations.conversations); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + Conversations finalConversations = conversations; + Runnable myRunnable = () -> conversationListMutableLiveData.setValue(finalConversations); + mainHandler.post(myRunnable); + }).start(); + + return conversationListMutableLiveData; + } + + /** + * Remove conversation + * + * @param id ID of the conversation + */ + public void deleteConversation(@NonNull String instance, String token, @NonNull String id) { + MastodonTimelinesService mastodonTimelinesService = init(instance); + new Thread(() -> { + Call deleteStatusCall = mastodonTimelinesService.deleteConversation(token, id); + if (deleteStatusCall != null) { + try { + deleteStatusCall.execute(); + } catch (IOException e) { + e.printStackTrace(); + } + } + }).start(); + } + + /** + * Mark a conversation as read + * + * @param id ID of the conversation + * @return {@link LiveData} containing a {@link Status} + */ + public LiveData markReadConversation(@NonNull String instance, String token, @NonNull String id) { + statusMutableLiveData = new MutableLiveData<>(); + MastodonTimelinesService mastodonTimelinesService = init(instance); + new Thread(() -> { + Status status = null; + Call markReadConversationCall = mastodonTimelinesService.markReadConversation(token, id); + if (markReadConversationCall != null) { + try { + Response markReadConversationResponse = markReadConversationCall.execute(); + if (markReadConversationResponse.isSuccessful()) { + status = markReadConversationResponse.body(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + Status finalStatus = status; + Runnable myRunnable = () -> statusMutableLiveData.setValue(finalStatus); + mainHandler.post(myRunnable); + }).start(); + return statusMutableLiveData; + } + + /** + * Fetch all lists that the user owns. + * + * @return {@link LiveData} containing a {@link List} of {@link MastodonList}s + */ + public LiveData> getLists(@NonNull String instance, String token) { + mastodonListListMutableLiveData = new MutableLiveData<>(); + MastodonTimelinesService mastodonTimelinesService = init(instance); + new Thread(() -> { + List mastodonListList = null; + Call> getListsCall = mastodonTimelinesService.getLists(token); + if (getListsCall != null) { + try { + Response> getListsResponse = getListsCall.execute(); + if (getListsResponse.isSuccessful()) { + mastodonListList = getListsResponse.body(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + List finalMastodonListList = mastodonListList; + Runnable myRunnable = () -> mastodonListListMutableLiveData.setValue(finalMastodonListList); + mainHandler.post(myRunnable); + }).start(); + return mastodonListListMutableLiveData; + } + + /** + * Fetch the list with the given ID. Used for verifying the title of a list, + * and which replies to show within that list. + * + * @param id ID of the list + * @return {@link LiveData} containing a {@link MastodonList} + */ + public LiveData getList(@NonNull String instance, String token, @NonNull String id) { + mastodonListMutableLiveData = new MutableLiveData<>(); + MastodonTimelinesService mastodonTimelinesService = init(instance); + new Thread(() -> { + MastodonList mastodonList = null; + Call getListCall = mastodonTimelinesService.getList(token, id); + if (getListCall != null) { + try { + Response getListResponse = getListCall.execute(); + if (getListResponse.isSuccessful()) { + mastodonList = getListResponse.body(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + MastodonList finalMastodonList = mastodonList; + Runnable myRunnable = () -> mastodonListMutableLiveData.setValue(finalMastodonList); + mainHandler.post(myRunnable); + }).start(); + return mastodonListMutableLiveData; + } + + /** + * Create a new list. + * + * @param title The title of the list to be created. + * @param repliesPolicy Enumerable oneOf "followed", "list", "none". Defaults to "list". + * @return {@link LiveData} containing a {@link MastodonList} + */ + public LiveData createList(@NonNull String instance, String token, @NonNull String title, String repliesPolicy) { + mastodonListMutableLiveData = new MutableLiveData<>(); + MastodonTimelinesService mastodonTimelinesService = init(instance); + new Thread(() -> { + MastodonList mastodonList = null; + Call createListCall = mastodonTimelinesService.createList(token, title, repliesPolicy); + if (createListCall != null) { + try { + Response createListResponse = createListCall.execute(); + if (createListResponse.isSuccessful()) { + mastodonList = createListResponse.body(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + MastodonList finalMastodonList = mastodonList; + Runnable myRunnable = () -> mastodonListMutableLiveData.setValue(finalMastodonList); + mainHandler.post(myRunnable); + }).start(); + return mastodonListMutableLiveData; + } + + /** + * Change the title of a list, or which replies to show. + * + * @param id ID of the list + * @param title The title of the list to be updated. + * @param repliesPolicy Enumerable oneOf "followed", "list", "none". + * @return {@link LiveData} containing a {@link MastodonList} + */ + public LiveData updateList(@NonNull String instance, String token, @NonNull String id, String title, String repliesPolicy) { + mastodonListMutableLiveData = new MutableLiveData<>(); + MastodonTimelinesService mastodonTimelinesService = init(instance); + new Thread(() -> { + MastodonList mastodonList = null; + Call updateListCall = mastodonTimelinesService.updateList(token, id, title, repliesPolicy); + if (updateListCall != null) { + try { + Response updateListResponse = updateListCall.execute(); + if (updateListResponse.isSuccessful()) { + mastodonList = updateListResponse.body(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + MastodonList finalMastodonList = mastodonList; + Runnable myRunnable = () -> mastodonListMutableLiveData.setValue(finalMastodonList); + mainHandler.post(myRunnable); + }).start(); + return mastodonListMutableLiveData; + } + + /** + * Delete a list + * + * @param id ID of the list + */ + public void deleteList(@NonNull String instance, String token, @NonNull String id) { + MastodonTimelinesService mastodonTimelinesService = init(instance); + new Thread(() -> { + Call deleteListCall = mastodonTimelinesService.deleteList(token, id); + if (deleteListCall != null) { + try { + deleteListCall.execute(); + } catch (IOException e) { + e.printStackTrace(); + } + } + }).start(); + } + + /** + * View accounts in list + * + * @param id ID of the list + * @return {@link LiveData} containing a {@link List} of {@link Account}s + */ + public LiveData> getAccountsInList(@NonNull String instance, String token, @NonNull String id, String maxId, String sinceId, int limit) { + accountListMutableLiveData = new MutableLiveData<>(); + MastodonTimelinesService mastodonTimelinesService = init(instance); + new Thread(() -> { + List accountList = null; + Call> getAccountsInListCall = mastodonTimelinesService.getAccountsInList(token, id, maxId, sinceId, limit); + if (getAccountsInListCall != null) { + try { + Response> getAccountsInListResponse = getAccountsInListCall.execute(); + if (getAccountsInListResponse.isSuccessful()) { + accountList = getAccountsInListResponse.body(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + List finalAccountList = accountList; + Runnable myRunnable = () -> accountListMutableLiveData.setValue(finalAccountList); + mainHandler.post(myRunnable); + }).start(); + return accountListMutableLiveData; + } + + /** + * Add accounts to the given list. Note that the user must be following these accounts. + * + * @param listId ID of the list + * @param accountIds Array of account IDs to add to the list. + */ + public void addAccountsList(@NonNull String instance, String token, @NonNull String listId, @NonNull List accountIds) { + MastodonTimelinesService mastodonTimelinesService = init(instance); + new Thread(() -> { + Call addAccountsListCall = mastodonTimelinesService.addAccountsList(token, listId, accountIds); + if (addAccountsListCall != null) { + try { + addAccountsListCall.execute(); + } catch (IOException e) { + e.printStackTrace(); + } + } + }).start(); + } + + /** + * Remove accounts from the given list. + * + * @param listId ID of the list + * @param accountIds Array of account IDs to remove from the list. + */ + public void deleteAccountsList(@NonNull String instance, String token, @NonNull String listId, @NonNull List accountIds) { + MastodonTimelinesService mastodonTimelinesService = init(instance); + new Thread(() -> { + Call deleteAccountsListCall = mastodonTimelinesService.deleteAccountsList(token, listId, accountIds); + if (deleteAccountsListCall != null) { + try { + deleteAccountsListCall.execute(); + } catch (IOException e) { + e.printStackTrace(); + } + } + }).start(); + } + + /** + * Get saved timeline position + * + * @param timeline Array of markers to fetch. String enum anyOf "home", "notifications". + * If not provided, an empty object will be returned. + * @return {@link LiveData} containing a {@link Marker} + */ + public LiveData getMarker(@NonNull String instance, String token, @NonNull List timeline) { + markerMutableLiveData = new MutableLiveData<>(); + MastodonTimelinesService mastodonTimelinesService = init(instance); + new Thread(() -> { + Marker marker = null; + Call getMarkerCall = mastodonTimelinesService.getMarker(token, timeline); + if (getMarkerCall != null) { + try { + Response getMarkerResponse = getMarkerCall.execute(); + if (getMarkerResponse.isSuccessful()) { + marker = getMarkerResponse.body(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + Marker finalMarker = marker; + Runnable myRunnable = () -> markerMutableLiveData.setValue(finalMarker); + mainHandler.post(myRunnable); + }).start(); + return markerMutableLiveData; + } + + /** + * Save position in timeline + * + * @param homeLastReadId ID of the last status read in the home timeline. + * @param notificationLastReadId ID of the last notification read. + */ + public void addMarker(@NonNull String instance, String token, String homeLastReadId, String notificationLastReadId) { + MastodonTimelinesService mastodonTimelinesService = init(instance); + new Thread(() -> { + Call addMarkerCall = mastodonTimelinesService.addMarker(token, homeLastReadId, notificationLastReadId); + if (addMarkerCall != null) { + try { + addMarkerCall.execute(); + } catch (IOException e) { + e.printStackTrace(); + } + } + }).start(); + } +} diff --git a/app/src/main/java/app/fedilab/android/viewmodel/mastodon/TopBarVM.java b/app/src/main/java/app/fedilab/android/viewmodel/mastodon/TopBarVM.java new file mode 100644 index 000000000..87ed69faf --- /dev/null +++ b/app/src/main/java/app/fedilab/android/viewmodel/mastodon/TopBarVM.java @@ -0,0 +1,56 @@ +package app.fedilab.android.viewmodel.mastodon; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import android.app.Application; +import android.os.Handler; +import android.os.Looper; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import app.fedilab.android.BaseMainActivity; +import app.fedilab.android.client.entities.Pinned; +import app.fedilab.android.exception.DBException; + +public class TopBarVM extends AndroidViewModel { + + private MutableLiveData pinnedMutableLiveData; + + public TopBarVM(@NonNull Application application) { + super(application); + } + + public LiveData getDBPinned() { + pinnedMutableLiveData = new MutableLiveData<>(); + new Thread(() -> { + Pinned pinned = new Pinned(getApplication().getApplicationContext()); + Handler mainHandler = new Handler(Looper.getMainLooper()); + Pinned pinnedTimeline = null; + try { + pinnedTimeline = pinned.getPinned(BaseMainActivity.accountWeakReference.get()); + } catch (DBException e) { + e.printStackTrace(); + } + Pinned finalPinnedTimeline = pinnedTimeline; + Runnable myRunnable = () -> pinnedMutableLiveData.setValue(finalPinnedTimeline); + mainHandler.post(myRunnable); + }).start(); + return pinnedMutableLiveData; + } + +} diff --git a/app/src/main/java/app/fedilab/android/webview/CustomWebview.java b/app/src/main/java/app/fedilab/android/webview/CustomWebview.java new file mode 100644 index 000000000..e53ea9c15 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/webview/CustomWebview.java @@ -0,0 +1,52 @@ +package app.fedilab.android.webview; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.Configuration; +import android.os.Build; +import android.util.AttributeSet; +import android.webkit.WebView; + +public class CustomWebview extends WebView { + + + public CustomWebview(Context context) { + super(getFixedContext(context)); + } + + public CustomWebview(Context context, AttributeSet attrs) { + super(getFixedContext(context), attrs); + } + + public CustomWebview(Context context, AttributeSet attrs, int defStyleAttr) { + super(getFixedContext(context), attrs, defStyleAttr); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public CustomWebview(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(getFixedContext(context), attrs, defStyleAttr, defStyleRes); + } + + @SuppressWarnings("deprecation") + public CustomWebview(Context context, AttributeSet attrs, int defStyleAttr, boolean privateBrowsing) { + super(getFixedContext(context), attrs, defStyleAttr, privateBrowsing); + } + + public static Context getFixedContext(Context context) { + return context.createConfigurationContext(new Configuration()); + } +} diff --git a/app/src/main/java/app/fedilab/android/webview/FedilabWebChromeClient.java b/app/src/main/java/app/fedilab/android/webview/FedilabWebChromeClient.java new file mode 100644 index 000000000..22f1a9555 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/webview/FedilabWebChromeClient.java @@ -0,0 +1,228 @@ +package app.fedilab.android.webview; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import android.app.Activity; +import android.graphics.Bitmap; +import android.media.MediaPlayer; +import android.view.LayoutInflater; +import android.view.SurfaceView; +import android.view.View; +import android.view.ViewGroup; +import android.webkit.WebChromeClient; +import android.webkit.WebView; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.ProgressBar; +import android.widget.TextView; + +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AppCompatActivity; + +import app.fedilab.android.R; + + +public class FedilabWebChromeClient extends WebChromeClient implements MediaPlayer.OnCompletionListener, MediaPlayer.OnErrorListener { + + private final CustomWebview webView; + private final View activityNonVideoView; + private final ViewGroup activityVideoView; + private final ProgressBar pbar; + private final Activity activity; + private FrameLayout videoViewContainer; + private CustomViewCallback videoViewCallback; + private ToggledFullscreenCallback toggledFullscreenCallback; + private boolean isVideoFullscreen; + + + public FedilabWebChromeClient(Activity activity, CustomWebview webView, FrameLayout activityNonVideoView, ViewGroup activityVideoView) { + this.activity = activity; + this.isVideoFullscreen = false; + this.webView = webView; + this.pbar = activity.findViewById(R.id.progress_bar); + this.activityNonVideoView = activityNonVideoView; + this.activityVideoView = activityVideoView; + } + + @Override + public void onProgressChanged(WebView view, int progress) { + if (pbar != null) { + if (progress < 100 && pbar.getVisibility() == ProgressBar.GONE) { + pbar.setVisibility(ProgressBar.VISIBLE); + } + pbar.setProgress(progress); + if (progress == 100) { + pbar.setVisibility(ProgressBar.GONE); + } + } + } + + @Override + public Bitmap getDefaultVideoPoster() { + return Bitmap.createBitmap(50, 50, Bitmap.Config.ARGB_8888); + } + + @Override + public void onReceivedIcon(WebView view, Bitmap icon) { + super.onReceivedIcon(view, icon); + LayoutInflater mInflater = LayoutInflater.from(activity); + ActionBar actionBar = ((AppCompatActivity) activity).getSupportActionBar(); + if (actionBar != null) { + View webview_actionbar = mInflater.inflate(R.layout.webview_actionbar, new LinearLayout(activity), false); + TextView webview_title = webview_actionbar.findViewById(R.id.webview_title); + webview_title.setText(view.getTitle()); + ImageView webview_favicon = webview_actionbar.findViewById(R.id.webview_favicon); + if (icon != null) + webview_favicon.setImageBitmap(icon); + actionBar.setCustomView(webview_actionbar); + actionBar.setDisplayShowCustomEnabled(true); + } else { + activity.setTitle(view.getTitle()); + } + + } + + /** + * Set a callback that will be fired when the video starts or finishes displaying using a custom view (typically full-screen) + * + * @param callback A VideoEnabledWebChromeClient.ToggledFullscreenCallback callback + */ + public void setOnToggledFullscreen(ToggledFullscreenCallback callback) { + this.toggledFullscreenCallback = callback; + } + + //FULLSCREEN VIDEO + //Code from https://stackoverflow.com/a/16179544/3197259 + + @Override + public void onShowCustomView(View view, CustomViewCallback callback) { + if (view instanceof FrameLayout) { + if (((AppCompatActivity) activity).getSupportActionBar() != null) + //noinspection ConstantConditions + ((AppCompatActivity) activity).getSupportActionBar().hide(); + // A video wants to be shown + FrameLayout frameLayout = (FrameLayout) view; + View focusedChild = frameLayout.getFocusedChild(); + + // Save video related variables + isVideoFullscreen = true; + this.videoViewContainer = frameLayout; + this.videoViewCallback = callback; + + // Hide the non-video view, add the video view, and show it + activityNonVideoView.setVisibility(View.INVISIBLE); + activityVideoView.addView(videoViewContainer, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + activityVideoView.setVisibility(View.VISIBLE); + if (focusedChild instanceof android.widget.VideoView) { + // android.widget.VideoView (typically API level <11) + android.widget.VideoView videoView = (android.widget.VideoView) focusedChild; + // Handle all the required events + videoView.setOnCompletionListener(this); + videoView.setOnErrorListener(this); + } else { + // Other classes, including: + // - android.webkit.HTML5VideoFullScreen$VideoSurfaceView, which inherits from android.view.SurfaceView (typically API level 11-18) + // - android.webkit.HTML5VideoFullScreen$VideoTextureView, which inherits from android.view.TextureView (typically API level 11-18) + // - com.android.org.chromium.content.browser.ContentVideoView$VideoSurfaceView, which inherits from android.view.SurfaceView (typically API level 19+) + + // Handle HTML5 video ended event only if the class is a SurfaceView + // Test case: TextureView of Sony Xperia T API level 16 doesn't work fullscreen when loading the javascript below + if (webView != null && webView.getSettings().getJavaScriptEnabled() && focusedChild instanceof SurfaceView) { + // Run javascript code that detects the video end and notifies the Javascript interface + String js = "javascript:"; + js += "var _ytrp_html5_video_last;"; + js += "var _ytrp_html5_video = document.getElementsByTagName('video')[0];"; + js += "if (_ytrp_html5_video != undefined && _ytrp_html5_video != _ytrp_html5_video_last) {"; + { + js += "_ytrp_html5_video_last = _ytrp_html5_video;"; + js += "function _ytrp_html5_video_ended() {"; + { + js += "_VideoEnabledWebView.notifyVideoEnd();"; // Must match Javascript interface name and method of VideoEnableWebView + } + js += "}"; + js += "_ytrp_html5_video.addEventListener('ended', _ytrp_html5_video_ended);"; + } + js += "}"; + webView.loadUrl(js); + } + } + // Notify full-screen change + if (toggledFullscreenCallback != null) { + toggledFullscreenCallback.toggledFullscreen(true); + } + } + } + + // Available in API level 14+, deprecated in API level 18+ + @Override + @SuppressWarnings("deprecation") + public void onShowCustomView(View view, int requestedOrientation, CustomViewCallback callback) { + onShowCustomView(view, callback); + } + + @Override + public void onHideCustomView() { + if (((AppCompatActivity) activity).getSupportActionBar() != null) + //noinspection ConstantConditions + ((AppCompatActivity) activity).getSupportActionBar().show(); + // This method should be manually called on video end in all cases because it's not always called automatically. + // This method must be manually called on back key press (from this class' onBackPressed() method). + if (isVideoFullscreen) { + // Hide the video view, remove it, and show the non-video view + activityVideoView.setVisibility(View.INVISIBLE); + activityVideoView.removeView(videoViewContainer); + activityNonVideoView.setVisibility(View.VISIBLE); + // Call back (only in API level <19, because in API level 19+ with chromium webview it crashes) + if (videoViewCallback != null && !videoViewCallback.getClass().getName().contains(".chromium.")) { + videoViewCallback.onCustomViewHidden(); + } + + // Reset video related variables + isVideoFullscreen = false; + videoViewContainer = null; + videoViewCallback = null; + + // Notify full-screen change + if (toggledFullscreenCallback != null) { + toggledFullscreenCallback.toggledFullscreen(false); + } + } + } + + // Video will start loading + @Override + public View getVideoLoadingProgressView() { + return super.getVideoLoadingProgressView(); + } + + // Video finished playing, only called in the case of android.widget.VideoView (typically API level <11) + @Override + public void onCompletion(MediaPlayer mp) { + onHideCustomView(); + } + + // Error while playing video, only called in the case of android.widget.VideoView (typically API level <11) + @Override + public boolean onError(MediaPlayer mp, int what, int extra) { + return false; // By returning false, onCompletion() will be called + } + + public interface ToggledFullscreenCallback { + void toggledFullscreen(boolean fullscreen); + } + +} + diff --git a/app/src/main/java/app/fedilab/android/webview/FedilabWebViewClient.java b/app/src/main/java/app/fedilab/android/webview/FedilabWebViewClient.java new file mode 100644 index 000000000..f910d5fb3 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/webview/FedilabWebViewClient.java @@ -0,0 +1,156 @@ +package app.fedilab.android.webview; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + + +import android.app.Activity; +import android.graphics.Bitmap; +import android.net.http.SslError; +import android.view.LayoutInflater; +import android.view.View; +import android.webkit.SslErrorHandler; +import android.webkit.URLUtil; +import android.webkit.WebResourceResponse; +import android.webkit.WebView; +import android.webkit.WebViewClient; +import android.widget.LinearLayout; +import android.widget.TextView; + +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AppCompatActivity; + +import java.io.ByteArrayInputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.List; + +import app.fedilab.android.R; +import app.fedilab.android.activities.WebviewActivity; + + +public class FedilabWebViewClient extends WebViewClient { + + private final Activity activity; + public List domains = new ArrayList<>(); + private int count = 0; + + public FedilabWebViewClient(Activity activity) { + this.activity = activity; + } + + @Override + public void onPageFinished(WebView view, String url) { + super.onPageFinished(view, url); + } + + + @Override + public WebResourceResponse shouldInterceptRequest(final WebView view, String url) { + if (WebviewActivity.trackingDomains != null) { + URI uri; + try { + uri = new URI(url); + String domain = uri.getHost(); + if (domain != null) { + domain = domain.startsWith("www.") ? domain.substring(4) : domain; + } + if (domain != null && WebviewActivity.trackingDomains.contains(domain)) { + if (activity instanceof WebviewActivity) { + count++; + domains.add(url); + // ((WebviewActivity) activity).setCount(activity, String.valueOf(count)); + } + ByteArrayInputStream nothing = new ByteArrayInputStream("".getBytes()); + return new WebResourceResponse("text/plain", "utf-8", nothing); + } + } catch (URISyntaxException e) { + try { + if (url.length() > 50) { + url = url.substring(0, 50); + } + uri = new URI(url); + String domain = uri.getHost(); + if (domain != null) { + domain = domain.startsWith("www.") ? domain.substring(4) : domain; + } + if (domain != null && WebviewActivity.trackingDomains.contains(domain)) { + if (activity instanceof WebviewActivity) { + count++; + domains.add(url); + // ((WebviewActivity) activity).setCount(activity, String.valueOf(count)); + } + ByteArrayInputStream nothing = new ByteArrayInputStream("".getBytes()); + return new WebResourceResponse("text/plain", "utf-8", nothing); + + } + } catch (URISyntaxException ignored) { + } + } + } + return super.shouldInterceptRequest(view, url); + } + + public List getDomains() { + return this.domains; + } + + @Override + public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) { + if (view.getUrl() != null && view.getUrl().endsWith(".onion")) { + handler.proceed(); + } else { + super.onReceivedSslError(view, handler, error); + } + } + + @Override + public boolean shouldOverrideUrlLoading(WebView view, String url) { + if (URLUtil.isNetworkUrl(url)) { + return false; + } else { + view.stopLoading(); + view.goBack(); + } + return false; + } + + @Override + public void onPageStarted(WebView view, String url, Bitmap favicon) { + super.onPageStarted(view, url, favicon); + count = 0; + domains = new ArrayList<>(); + domains.clear(); + ActionBar actionBar = ((AppCompatActivity) activity).getSupportActionBar(); + LayoutInflater mInflater = LayoutInflater.from(activity); + if (actionBar != null) { + View webview_actionbar = mInflater.inflate(R.layout.webview_actionbar, new LinearLayout(activity), false); + TextView webview_title = webview_actionbar.findViewById(R.id.webview_title); + webview_title.setText(url); + actionBar.setCustomView(webview_actionbar); + actionBar.setDisplayShowCustomEnabled(true); + } else { + activity.setTitle(url); + } + //Changes the url in webview activity so that it can be opened with an external app + try { + if (activity instanceof WebviewActivity) + ((WebviewActivity) activity).setUrl(url); + } catch (Exception ignore) { + } + + } + +} diff --git a/app/src/main/java/app/fedilab/android/webview/ProxyHelper.java b/app/src/main/java/app/fedilab/android/webview/ProxyHelper.java new file mode 100644 index 000000000..1e94a6c80 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/webview/ProxyHelper.java @@ -0,0 +1,166 @@ +package app.fedilab.android.webview; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.Intent; +import android.net.Proxy; +import android.util.ArrayMap; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +public class ProxyHelper { + + + public static void setProxy(Context context, CustomWebview webview, String host, int port, String applicationClassName) { + + setProxyKKPlus(context, webview, host, port, applicationClassName); + } + + + @SuppressWarnings("all") + private static boolean setProxyICS(CustomWebview webview, String host, int port) { + try { + Class jwcjb = Class.forName("android.webkit.JWebCoreJavaBridge"); + Class params[] = new Class[1]; + params[0] = Class.forName("android.net.ProxyProperties"); + Method updateProxyInstance = jwcjb.getDeclaredMethod("updateProxy", params); + + Class wv = Class.forName("android.webkit.WebView"); + Field mWebViewCoreField = wv.getDeclaredField("mWebViewCore"); + Object mWebViewCoreFieldInstance = getFieldValueSafely(mWebViewCoreField, webview); + + Class wvc = Class.forName("android.webkit.WebViewCore"); + Field mBrowserFrameField = wvc.getDeclaredField("mBrowserFrame"); + Object mBrowserFrame = getFieldValueSafely(mBrowserFrameField, mWebViewCoreFieldInstance); + + Class bf = Class.forName("android.webkit.BrowserFrame"); + Field sJavaBridgeField = bf.getDeclaredField("sJavaBridge"); + Object sJavaBridge = getFieldValueSafely(sJavaBridgeField, mBrowserFrame); + + Class ppclass = Class.forName("android.net.ProxyProperties"); + Class pparams[] = new Class[3]; + pparams[0] = String.class; + pparams[1] = int.class; + pparams[2] = String.class; + Constructor ppcont = ppclass.getConstructor(pparams); + + updateProxyInstance.invoke(sJavaBridge, ppcont.newInstance(host, port, null)); + return true; + } catch (Exception ex) { + return false; + } + } + + /** + * Set Proxy for Android 4.1 - 4.3. + */ + @SuppressWarnings("all") + private static boolean setProxyJB(CustomWebview webview, String host, int port) { + + try { + Class wvcClass = Class.forName("android.webkit.WebViewClassic"); + Class wvParams[] = new Class[1]; + wvParams[0] = Class.forName("android.webkit.WebView"); + Method fromWebView = wvcClass.getDeclaredMethod("fromWebView", wvParams); + Object webViewClassic = fromWebView.invoke(null, webview); + + Class wv = Class.forName("android.webkit.WebViewClassic"); + Field mWebViewCoreField = wv.getDeclaredField("mWebViewCore"); + Object mWebViewCoreFieldInstance = getFieldValueSafely(mWebViewCoreField, webViewClassic); + + Class wvc = Class.forName("android.webkit.WebViewCore"); + Field mBrowserFrameField = wvc.getDeclaredField("mBrowserFrame"); + Object mBrowserFrame = getFieldValueSafely(mBrowserFrameField, mWebViewCoreFieldInstance); + + Class bf = Class.forName("android.webkit.BrowserFrame"); + Field sJavaBridgeField = bf.getDeclaredField("sJavaBridge"); + Object sJavaBridge = getFieldValueSafely(sJavaBridgeField, mBrowserFrame); + + Class ppclass = Class.forName("android.net.ProxyProperties"); + Class pparams[] = new Class[3]; + pparams[0] = String.class; + pparams[1] = int.class; + pparams[2] = String.class; + Constructor ppcont = ppclass.getConstructor(pparams); + + Class jwcjb = Class.forName("android.webkit.JWebCoreJavaBridge"); + Class params[] = new Class[1]; + params[0] = Class.forName("android.net.ProxyProperties"); + Method updateProxyInstance = jwcjb.getDeclaredMethod("updateProxy", params); + + updateProxyInstance.invoke(sJavaBridge, ppcont.newInstance(host, port, null)); + } catch (Exception ex) { + return false; + } + return true; + } + + // from https://stackoverflow.com/questions/19979578/android-webview-set-proxy-programatically-kitkat + @SuppressLint("NewApi") + @SuppressWarnings("all") + private static void setProxyKKPlus(Context appContext, CustomWebview webView, String host, int port, String applicationClassName) { + + System.setProperty("http.proxyHost", host); + System.setProperty("http.proxyPort", port + ""); + System.setProperty("https.proxyHost", host); + System.setProperty("https.proxyPort", port + ""); + try { + Class applictionCls = Class.forName("android.app.Application"); + Field loadedApkField = applictionCls.getDeclaredField("mLoadedApk"); + loadedApkField.setAccessible(true); + Object loadedApk = loadedApkField.get(appContext); + Class loadedApkCls = Class.forName("android.app.LoadedApk"); + Field receiversField = loadedApkCls.getDeclaredField("mReceivers"); + receiversField.setAccessible(true); + ArrayMap receivers = (ArrayMap) receiversField.get(loadedApk); + for (Object receiverMap : receivers.values()) { + for (Object rec : ((ArrayMap) receiverMap).keySet()) { + Class clazz = rec.getClass(); + if (clazz.getName().contains("ProxyChangeListener")) { + Method onReceiveMethod = clazz.getDeclaredMethod("onReceive", Context.class, Intent.class); + Intent intent = new Intent(Proxy.PROXY_CHANGE_ACTION); + + onReceiveMethod.invoke(rec, appContext, intent); + } + } + } + } catch (ClassNotFoundException e) { + e.printStackTrace(); + } catch (NoSuchFieldException e) { + e.printStackTrace(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } catch (IllegalArgumentException e) { + e.printStackTrace(); + } catch (NoSuchMethodException e) { + e.printStackTrace(); + } catch (InvocationTargetException e) { + e.printStackTrace(); + } + } + + private static Object getFieldValueSafely(Field field, Object classInstance) throws IllegalArgumentException, IllegalAccessException { + boolean oldAccessibleValue = field.isAccessible(); + field.setAccessible(true); + Object result = field.get(classInstance); + field.setAccessible(oldAccessibleValue); + return result; + } +} diff --git a/app/src/main/res/anim/enter.xml b/app/src/main/res/anim/enter.xml new file mode 100644 index 000000000..76bfc44c3 --- /dev/null +++ b/app/src/main/res/anim/enter.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/app/src/main/res/anim/exit.xml b/app/src/main/res/anim/exit.xml new file mode 100644 index 000000000..94a8bc808 --- /dev/null +++ b/app/src/main/res/anim/exit.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/app/src/main/res/anim/pop_enter.xml b/app/src/main/res/anim/pop_enter.xml new file mode 100644 index 000000000..136c23d57 --- /dev/null +++ b/app/src/main/res/anim/pop_enter.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/app/src/main/res/anim/pop_exit.xml b/app/src/main/res/anim/pop_exit.xml new file mode 100644 index 000000000..1154aa610 --- /dev/null +++ b/app/src/main/res/anim/pop_exit.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/app/src/main/res/color/my_button_colors.xml b/app/src/main/res/color/my_button_colors.xml new file mode 100644 index 000000000..4ca986309 --- /dev/null +++ b/app/src/main/res/color/my_button_colors.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-anydpi-v24/ic_notification.xml b/app/src/main/res/drawable-anydpi-v24/ic_notification.xml new file mode 100644 index 000000000..6c06f3ad5 --- /dev/null +++ b/app/src/main/res/drawable-anydpi-v24/ic_notification.xml @@ -0,0 +1,19 @@ + + + + + + diff --git a/app/src/main/res/drawable-hdpi/mastodon_icon.png b/app/src/main/res/drawable-hdpi/mastodon_icon.png new file mode 100644 index 000000000..c16328243 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/mastodon_icon.png differ diff --git a/app/src/main/res/drawable-hdpi/mastodon_icon_item.png b/app/src/main/res/drawable-hdpi/mastodon_icon_item.png new file mode 100644 index 000000000..a65e4ee8f Binary files /dev/null and b/app/src/main/res/drawable-hdpi/mastodon_icon_item.png differ diff --git a/app/src/main/res/drawable-hdpi/misskey.png b/app/src/main/res/drawable-hdpi/misskey.png new file mode 100644 index 000000000..2961e94fa Binary files /dev/null and b/app/src/main/res/drawable-hdpi/misskey.png differ diff --git a/app/src/main/res/drawable-hdpi/peertube_icon.png b/app/src/main/res/drawable-hdpi/peertube_icon.png new file mode 100644 index 000000000..d65ff271b Binary files /dev/null and b/app/src/main/res/drawable-hdpi/peertube_icon.png differ diff --git a/app/src/main/res/drawable-hdpi/pixelfed.png b/app/src/main/res/drawable-hdpi/pixelfed.png new file mode 100644 index 000000000..62324da98 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/pixelfed.png differ diff --git a/app/src/main/res/drawable-mdpi/mastodon_icon.png b/app/src/main/res/drawable-mdpi/mastodon_icon.png new file mode 100644 index 000000000..bf39d98a1 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/mastodon_icon.png differ diff --git a/app/src/main/res/drawable-mdpi/mastodon_icon_item.png b/app/src/main/res/drawable-mdpi/mastodon_icon_item.png new file mode 100644 index 000000000..34bea8337 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/mastodon_icon_item.png differ diff --git a/app/src/main/res/drawable-mdpi/misskey.png b/app/src/main/res/drawable-mdpi/misskey.png new file mode 100644 index 000000000..1e1192d85 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/misskey.png differ diff --git a/app/src/main/res/drawable-mdpi/peertube_icon.png b/app/src/main/res/drawable-mdpi/peertube_icon.png new file mode 100644 index 000000000..2c5df945a Binary files /dev/null and b/app/src/main/res/drawable-mdpi/peertube_icon.png differ diff --git a/app/src/main/res/drawable-mdpi/pixelfed.png b/app/src/main/res/drawable-mdpi/pixelfed.png new file mode 100644 index 000000000..d86bbb52f Binary files /dev/null and b/app/src/main/res/drawable-mdpi/pixelfed.png differ diff --git a/app/src/main/res/drawable-xhdpi/mastodon_icon.png b/app/src/main/res/drawable-xhdpi/mastodon_icon.png new file mode 100644 index 000000000..d6652d11a Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/mastodon_icon.png differ diff --git a/app/src/main/res/drawable-xhdpi/mastodon_icon_item.png b/app/src/main/res/drawable-xhdpi/mastodon_icon_item.png new file mode 100644 index 000000000..bd01edc13 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/mastodon_icon_item.png differ diff --git a/app/src/main/res/drawable-xhdpi/misskey.png b/app/src/main/res/drawable-xhdpi/misskey.png new file mode 100644 index 000000000..0d36e8cb0 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/misskey.png differ diff --git a/app/src/main/res/drawable-xhdpi/peertube_icon.png b/app/src/main/res/drawable-xhdpi/peertube_icon.png new file mode 100644 index 000000000..fd4d5b772 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/peertube_icon.png differ diff --git a/app/src/main/res/drawable-xhdpi/pixelfed.png b/app/src/main/res/drawable-xhdpi/pixelfed.png new file mode 100644 index 000000000..141c477e6 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/pixelfed.png differ diff --git a/app/src/main/res/drawable-xxhdpi/mastodon_icon.png b/app/src/main/res/drawable-xxhdpi/mastodon_icon.png new file mode 100644 index 000000000..1b28aedc7 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/mastodon_icon.png differ diff --git a/app/src/main/res/drawable-xxhdpi/mastodon_icon_item.png b/app/src/main/res/drawable-xxhdpi/mastodon_icon_item.png new file mode 100644 index 000000000..33b0ee6b3 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/mastodon_icon_item.png differ diff --git a/app/src/main/res/drawable-xxhdpi/misskey.png b/app/src/main/res/drawable-xxhdpi/misskey.png new file mode 100644 index 000000000..5218d7f4b Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/misskey.png differ diff --git a/app/src/main/res/drawable-xxhdpi/peertube_icon.png b/app/src/main/res/drawable-xxhdpi/peertube_icon.png new file mode 100644 index 000000000..931d894b9 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/peertube_icon.png differ diff --git a/app/src/main/res/drawable-xxhdpi/pixelfed.png b/app/src/main/res/drawable-xxhdpi/pixelfed.png new file mode 100644 index 000000000..a2cf0f68f Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/pixelfed.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/mastodon_icon.png b/app/src/main/res/drawable-xxxhdpi/mastodon_icon.png new file mode 100644 index 000000000..298515f1e Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/mastodon_icon.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/mastodon_icon_item.png b/app/src/main/res/drawable-xxxhdpi/mastodon_icon_item.png new file mode 100644 index 000000000..257515de1 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/mastodon_icon_item.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/misskey.png b/app/src/main/res/drawable-xxxhdpi/misskey.png new file mode 100644 index 000000000..d1863aab3 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/misskey.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/peertube_icon.png b/app/src/main/res/drawable-xxxhdpi/peertube_icon.png new file mode 100644 index 000000000..1ee562168 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/peertube_icon.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/pixelfed.png b/app/src/main/res/drawable-xxxhdpi/pixelfed.png new file mode 100644 index 000000000..49703d27a Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/pixelfed.png differ diff --git a/app/src/main/res/drawable/blue_border.xml b/app/src/main/res/drawable/blue_border.xml new file mode 100644 index 000000000..74aeb3419 --- /dev/null +++ b/app/src/main/res/drawable/blue_border.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/decoration.xml b/app/src/main/res/drawable/decoration.xml new file mode 100644 index 000000000..dde8c272f --- /dev/null +++ b/app/src/main/res/drawable/decoration.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/default_banner.xml b/app/src/main/res/drawable/default_banner.xml new file mode 100644 index 000000000..2324a79d7 --- /dev/null +++ b/app/src/main/res/drawable/default_banner.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/fedilab_logo_bubbles.xml b/app/src/main/res/drawable/fedilab_logo_bubbles.xml new file mode 100644 index 000000000..01e2fc41e --- /dev/null +++ b/app/src/main/res/drawable/fedilab_logo_bubbles.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/green_border.xml b/app/src/main/res/drawable/green_border.xml new file mode 100644 index 000000000..7cd9ec943 --- /dev/null +++ b/app/src/main/res/drawable/green_border.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_baseline_access_time_24.xml b/app/src/main/res/drawable/ic_baseline_access_time_24.xml new file mode 100644 index 000000000..968a42eb3 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_access_time_24.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/drawable/ic_baseline_access_time_filled_24.xml b/app/src/main/res/drawable/ic_baseline_access_time_filled_24.xml new file mode 100644 index 000000000..ffa857a5d --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_access_time_filled_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_add_24.xml b/app/src/main/res/drawable/ic_baseline_add_24.xml new file mode 100644 index 000000000..eb232541d --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_add_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_arrow_drop_down_24.xml b/app/src/main/res/drawable/ic_baseline_arrow_drop_down_24.xml new file mode 100644 index 000000000..3e5dc77e2 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_arrow_drop_down_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_arrow_drop_up_24.xml b/app/src/main/res/drawable/ic_baseline_arrow_drop_up_24.xml new file mode 100644 index 000000000..e88c221d4 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_arrow_drop_up_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_audio_file_24.xml b/app/src/main/res/drawable/ic_baseline_audio_file_24.xml new file mode 100644 index 000000000..bb30a0596 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_audio_file_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_block_24.xml b/app/src/main/res/drawable/ic_baseline_block_24.xml new file mode 100644 index 000000000..9fefeec67 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_block_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_camera_alt_24.xml b/app/src/main/res/drawable/ic_baseline_camera_alt_24.xml new file mode 100644 index 000000000..4bef5ddc1 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_camera_alt_24.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/drawable/ic_baseline_card_travel_24.xml b/app/src/main/res/drawable/ic_baseline_card_travel_24.xml new file mode 100644 index 000000000..3174f1b64 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_card_travel_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_chat_bubble_24.xml b/app/src/main/res/drawable/ic_baseline_chat_bubble_24.xml new file mode 100644 index 000000000..8e797a5e7 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_chat_bubble_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_chat_bubble_outline_24.xml b/app/src/main/res/drawable/ic_baseline_chat_bubble_outline_24.xml new file mode 100644 index 000000000..92ff42fbf --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_chat_bubble_outline_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_check_24.xml b/app/src/main/res/drawable/ic_baseline_check_24.xml new file mode 100644 index 000000000..0432fa69b --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_check_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_check_circle_24.xml b/app/src/main/res/drawable/ic_baseline_check_circle_24.xml new file mode 100644 index 000000000..5e111ca7d --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_check_circle_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_chevron_right_24.xml b/app/src/main/res/drawable/ic_baseline_chevron_right_24.xml new file mode 100644 index 000000000..f8eaa4089 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_chevron_right_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_close_24.xml b/app/src/main/res/drawable/ic_baseline_close_24.xml new file mode 100644 index 000000000..16d6d37dd --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_close_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_contact_page_24.xml b/app/src/main/res/drawable/ic_baseline_contact_page_24.xml new file mode 100644 index 000000000..edb692e9a --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_contact_page_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_delete_24.xml b/app/src/main/res/drawable/ic_baseline_delete_24.xml new file mode 100644 index 000000000..79372b1d6 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_delete_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_drafts_24.xml b/app/src/main/res/drawable/ic_baseline_drafts_24.xml new file mode 100644 index 000000000..b16a79b3f --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_drafts_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_drag_handle_24.xml b/app/src/main/res/drawable/ic_baseline_drag_handle_24.xml new file mode 100644 index 000000000..b4c5e3c6f --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_drag_handle_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_edit_24.xml b/app/src/main/res/drawable/ic_baseline_edit_24.xml new file mode 100644 index 000000000..5fb90ad4f --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_edit_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_expand_less_24.xml b/app/src/main/res/drawable/ic_baseline_expand_less_24.xml new file mode 100644 index 000000000..f450c2f21 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_expand_less_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_expand_more_24.xml b/app/src/main/res/drawable/ic_baseline_expand_more_24.xml new file mode 100644 index 000000000..dc1bcac4e --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_expand_more_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_filter_list_24.xml b/app/src/main/res/drawable/ic_baseline_filter_list_24.xml new file mode 100644 index 000000000..52d959093 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_filter_list_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_first_page_24.xml b/app/src/main/res/drawable/ic_baseline_first_page_24.xml new file mode 100644 index 000000000..b4e170678 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_first_page_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_home_24.xml b/app/src/main/res/drawable/ic_baseline_home_24.xml new file mode 100644 index 000000000..3a4c7dac9 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_home_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_hourglass_full_24.xml b/app/src/main/res/drawable/ic_baseline_hourglass_full_24.xml new file mode 100644 index 000000000..2ac5af840 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_hourglass_full_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_info_24.xml b/app/src/main/res/drawable/ic_baseline_info_24.xml new file mode 100644 index 000000000..17255b7ae --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_info_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_insert_drive_file_24.xml b/app/src/main/res/drawable/ic_baseline_insert_drive_file_24.xml new file mode 100644 index 000000000..62c1d5e97 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_insert_drive_file_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_label_24.xml b/app/src/main/res/drawable/ic_baseline_label_24.xml new file mode 100644 index 000000000..3d77ced34 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_label_24.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_last_page_24.xml b/app/src/main/res/drawable/ic_baseline_last_page_24.xml new file mode 100644 index 000000000..5d609856a --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_last_page_24.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_lock_24.xml b/app/src/main/res/drawable/ic_baseline_lock_24.xml new file mode 100644 index 000000000..d6191026a --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_lock_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_lock_open_24.xml b/app/src/main/res/drawable/ic_baseline_lock_open_24.xml new file mode 100644 index 000000000..a11b70e62 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_lock_open_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_mail_24.xml b/app/src/main/res/drawable/ic_baseline_mail_24.xml new file mode 100644 index 000000000..6943b4ced --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_mail_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_mail_outline_24.xml b/app/src/main/res/drawable/ic_baseline_mail_outline_24.xml new file mode 100644 index 000000000..fb0ed8f15 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_mail_outline_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_message_24.xml b/app/src/main/res/drawable/ic_baseline_message_24.xml new file mode 100644 index 000000000..c9a3e3daf --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_message_24.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_mic_24.xml b/app/src/main/res/drawable/ic_baseline_mic_24.xml new file mode 100644 index 000000000..1e8710bac --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_mic_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_mode_24.xml b/app/src/main/res/drawable/ic_baseline_mode_24.xml new file mode 100644 index 000000000..a02257b3a --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_mode_24.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/drawable/ic_baseline_more_vert_24.xml b/app/src/main/res/drawable/ic_baseline_more_vert_24.xml new file mode 100644 index 000000000..14ff51ef8 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_more_vert_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_navigate_next_24.xml b/app/src/main/res/drawable/ic_baseline_navigate_next_24.xml new file mode 100644 index 000000000..76251d055 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_navigate_next_24.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_note_24.xml b/app/src/main/res/drawable/ic_baseline_note_24.xml new file mode 100644 index 000000000..ecc9b05fa --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_note_24.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_notifications_24.xml b/app/src/main/res/drawable/ic_baseline_notifications_24.xml new file mode 100644 index 000000000..21cb88d1e --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_notifications_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_notifications_active_24.xml b/app/src/main/res/drawable/ic_baseline_notifications_active_24.xml new file mode 100644 index 000000000..70d2420be --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_notifications_active_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_notifications_off_24.xml b/app/src/main/res/drawable/ic_baseline_notifications_off_24.xml new file mode 100644 index 000000000..5817bca17 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_notifications_off_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_open_with_24.xml b/app/src/main/res/drawable/ic_baseline_open_with_24.xml new file mode 100644 index 000000000..6f602a3de --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_open_with_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_people_alt_24.xml b/app/src/main/res/drawable/ic_baseline_people_alt_24.xml new file mode 100644 index 000000000..58169a9aa --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_people_alt_24.xml @@ -0,0 +1,23 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_baseline_perm_media_24.xml b/app/src/main/res/drawable/ic_baseline_perm_media_24.xml new file mode 100644 index 000000000..c47c42de6 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_perm_media_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_person_add_24.xml b/app/src/main/res/drawable/ic_baseline_person_add_24.xml new file mode 100644 index 000000000..842c6df39 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_person_add_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_person_add_alt_1_24.xml b/app/src/main/res/drawable/ic_baseline_person_add_alt_1_24.xml new file mode 100644 index 000000000..46998c45a --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_person_add_alt_1_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_person_remove_24.xml b/app/src/main/res/drawable/ic_baseline_person_remove_24.xml new file mode 100644 index 000000000..db943af65 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_person_remove_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_person_remove_alt_1_24.xml b/app/src/main/res/drawable/ic_baseline_person_remove_alt_1_24.xml new file mode 100644 index 000000000..b194211a2 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_person_remove_alt_1_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_playlist_add_24.xml b/app/src/main/res/drawable/ic_baseline_playlist_add_24.xml new file mode 100644 index 000000000..b7121d369 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_playlist_add_24.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_playlist_add_check_24.xml b/app/src/main/res/drawable/ic_baseline_playlist_add_check_24.xml new file mode 100644 index 000000000..2e766c9bf --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_playlist_add_check_24.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_baseline_poll_24.xml b/app/src/main/res/drawable/ic_baseline_poll_24.xml new file mode 100644 index 000000000..04849d5a2 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_poll_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_public_24.xml b/app/src/main/res/drawable/ic_baseline_public_24.xml new file mode 100644 index 000000000..19fb42596 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_public_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_refresh_24.xml b/app/src/main/res/drawable/ic_baseline_refresh_24.xml new file mode 100644 index 000000000..0d768f68d --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_refresh_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_remove_24.xml b/app/src/main/res/drawable/ic_baseline_remove_24.xml new file mode 100644 index 000000000..791a2f81a --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_remove_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_remove_red_eye_24.xml b/app/src/main/res/drawable/ic_baseline_remove_red_eye_24.xml new file mode 100644 index 000000000..4fca57646 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_remove_red_eye_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_reorder_24.xml b/app/src/main/res/drawable/ic_baseline_reorder_24.xml new file mode 100644 index 000000000..68334062c --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_reorder_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_repeat_24.xml b/app/src/main/res/drawable/ic_baseline_repeat_24.xml new file mode 100644 index 000000000..5b0f044ac --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_repeat_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_reply_24.xml b/app/src/main/res/drawable/ic_baseline_reply_24.xml new file mode 100644 index 000000000..be709aea1 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_reply_24.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_save_24.xml b/app/src/main/res/drawable/ic_baseline_save_24.xml new file mode 100644 index 000000000..36541ed28 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_save_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_schedule_24.xml b/app/src/main/res/drawable/ic_baseline_schedule_24.xml new file mode 100644 index 000000000..968a42eb3 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_schedule_24.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/drawable/ic_baseline_schedule_send_24.xml b/app/src/main/res/drawable/ic_baseline_schedule_send_24.xml new file mode 100644 index 000000000..94c60811e --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_schedule_send_24.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_search_24.xml b/app/src/main/res/drawable/ic_baseline_search_24.xml new file mode 100644 index 000000000..fdaf866e4 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_search_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_settings_24.xml b/app/src/main/res/drawable/ic_baseline_settings_24.xml new file mode 100644 index 000000000..78c2189b8 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_settings_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_share_24.xml b/app/src/main/res/drawable/ic_baseline_share_24.xml new file mode 100644 index 000000000..59b0e84dd --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_share_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_skip_next_24.xml b/app/src/main/res/drawable/ic_baseline_skip_next_24.xml new file mode 100644 index 000000000..79972afe5 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_skip_next_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_skip_previous_24.xml b/app/src/main/res/drawable/ic_baseline_skip_previous_24.xml new file mode 100644 index 000000000..00db3759c --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_skip_previous_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_star_24.xml b/app/src/main/res/drawable/ic_baseline_star_24.xml new file mode 100644 index 000000000..448ae51c5 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_star_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_stop_circle_64.xml b/app/src/main/res/drawable/ic_baseline_stop_circle_64.xml new file mode 100644 index 000000000..a1ae94a95 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_stop_circle_64.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_supervised_user_circle_24.xml b/app/src/main/res/drawable/ic_baseline_supervised_user_circle_24.xml new file mode 100644 index 000000000..61b9bbcde --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_supervised_user_circle_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_verified_24.xml b/app/src/main/res/drawable/ic_baseline_verified_24.xml new file mode 100644 index 000000000..251ca13c8 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_verified_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_view_list_24.xml b/app/src/main/res/drawable/ic_baseline_view_list_24.xml new file mode 100644 index 000000000..55ec20f77 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_view_list_24.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_visibility_24.xml b/app/src/main/res/drawable/ic_baseline_visibility_24.xml new file mode 100644 index 000000000..4fca57646 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_visibility_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_visibility_off_24.xml b/app/src/main/res/drawable/ic_baseline_visibility_off_24.xml new file mode 100644 index 000000000..d9a725769 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_visibility_off_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_volume_mute_24.xml b/app/src/main/res/drawable/ic_baseline_volume_mute_24.xml new file mode 100644 index 000000000..3f2233859 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_volume_mute_24.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_compose_attach.xml b/app/src/main/res/drawable/ic_compose_attach.xml new file mode 100644 index 000000000..7607bd195 --- /dev/null +++ b/app/src/main/res/drawable/ic_compose_attach.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_compose_attach_audio.xml b/app/src/main/res/drawable/ic_compose_attach_audio.xml new file mode 100644 index 000000000..ba1442961 --- /dev/null +++ b/app/src/main/res/drawable/ic_compose_attach_audio.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_compose_attach_image.xml b/app/src/main/res/drawable/ic_compose_attach_image.xml new file mode 100644 index 000000000..306717a9f --- /dev/null +++ b/app/src/main/res/drawable/ic_compose_attach_image.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_compose_attach_more.xml b/app/src/main/res/drawable/ic_compose_attach_more.xml new file mode 100644 index 000000000..8fd17f690 --- /dev/null +++ b/app/src/main/res/drawable/ic_compose_attach_more.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_compose_attach_video.xml b/app/src/main/res/drawable/ic_compose_attach_video.xml new file mode 100644 index 000000000..895d62520 --- /dev/null +++ b/app/src/main/res/drawable/ic_compose_attach_video.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_compose_attachment_description.xml b/app/src/main/res/drawable/ic_compose_attachment_description.xml new file mode 100644 index 000000000..5d37922a6 --- /dev/null +++ b/app/src/main/res/drawable/ic_compose_attachment_description.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_compose_attachment_order_down.xml b/app/src/main/res/drawable/ic_compose_attachment_order_down.xml new file mode 100644 index 000000000..3c211b16f --- /dev/null +++ b/app/src/main/res/drawable/ic_compose_attachment_order_down.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_compose_attachment_order_up.xml b/app/src/main/res/drawable/ic_compose_attachment_order_up.xml new file mode 100644 index 000000000..d163db56c --- /dev/null +++ b/app/src/main/res/drawable/ic_compose_attachment_order_up.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_compose_attachment_play.xml b/app/src/main/res/drawable/ic_compose_attachment_play.xml new file mode 100644 index 000000000..391d042bd --- /dev/null +++ b/app/src/main/res/drawable/ic_compose_attachment_play.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_compose_attachment_remove.xml b/app/src/main/res/drawable/ic_compose_attachment_remove.xml new file mode 100644 index 000000000..0f7dbfc9a --- /dev/null +++ b/app/src/main/res/drawable/ic_compose_attachment_remove.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_compose_emoji.xml b/app/src/main/res/drawable/ic_compose_emoji.xml new file mode 100644 index 000000000..5de5cd0e2 --- /dev/null +++ b/app/src/main/res/drawable/ic_compose_emoji.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_compose_poll.xml b/app/src/main/res/drawable/ic_compose_poll.xml new file mode 100644 index 000000000..705ac5b5e --- /dev/null +++ b/app/src/main/res/drawable/ic_compose_poll.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_compose_poll_option_mark_multiple.xml b/app/src/main/res/drawable/ic_compose_poll_option_mark_multiple.xml new file mode 100644 index 000000000..2752cc5aa --- /dev/null +++ b/app/src/main/res/drawable/ic_compose_poll_option_mark_multiple.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_compose_poll_option_mark_single.xml b/app/src/main/res/drawable/ic_compose_poll_option_mark_single.xml new file mode 100644 index 000000000..0baafd862 --- /dev/null +++ b/app/src/main/res/drawable/ic_compose_poll_option_mark_single.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_compose_post.xml b/app/src/main/res/drawable/ic_compose_post.xml new file mode 100644 index 000000000..c54d3aa8d --- /dev/null +++ b/app/src/main/res/drawable/ic_compose_post.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_compose_sensitive.xml b/app/src/main/res/drawable/ic_compose_sensitive.xml new file mode 100644 index 000000000..cc9f66f12 --- /dev/null +++ b/app/src/main/res/drawable/ic_compose_sensitive.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_compose_thread_add_status.xml b/app/src/main/res/drawable/ic_compose_thread_add_status.xml new file mode 100644 index 000000000..fe178cb55 --- /dev/null +++ b/app/src/main/res/drawable/ic_compose_thread_add_status.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_compose_thread_remove_status.xml b/app/src/main/res/drawable/ic_compose_thread_remove_status.xml new file mode 100644 index 000000000..5cac7be0e --- /dev/null +++ b/app/src/main/res/drawable/ic_compose_thread_remove_status.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_compose_visibility_direct.xml b/app/src/main/res/drawable/ic_compose_visibility_direct.xml new file mode 100644 index 000000000..2c8521771 --- /dev/null +++ b/app/src/main/res/drawable/ic_compose_visibility_direct.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_compose_visibility_private.xml b/app/src/main/res/drawable/ic_compose_visibility_private.xml new file mode 100644 index 000000000..8f13e378b --- /dev/null +++ b/app/src/main/res/drawable/ic_compose_visibility_private.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_compose_visibility_public.xml b/app/src/main/res/drawable/ic_compose_visibility_public.xml new file mode 100644 index 000000000..2c9d4e34c --- /dev/null +++ b/app/src/main/res/drawable/ic_compose_visibility_public.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_compose_visibility_unlisted.xml b/app/src/main/res/drawable/ic_compose_visibility_unlisted.xml new file mode 100644 index 000000000..3d34c2a86 --- /dev/null +++ b/app/src/main/res/drawable/ic_compose_visibility_unlisted.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_error.xml b/app/src/main/res/drawable/ic_error.xml new file mode 100644 index 000000000..bec7b88ba --- /dev/null +++ b/app/src/main/res/drawable/ic_error.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_gnu_social.xml b/app/src/main/res/drawable/ic_gnu_social.xml new file mode 100644 index 000000000..f58faffcc --- /dev/null +++ b/app/src/main/res/drawable/ic_gnu_social.xml @@ -0,0 +1,31 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 000000000..1f139b44b --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_menu_camera.xml b/app/src/main/res/drawable/ic_menu_camera.xml new file mode 100644 index 000000000..634fe9221 --- /dev/null +++ b/app/src/main/res/drawable/ic_menu_camera.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_menu_gallery.xml b/app/src/main/res/drawable/ic_menu_gallery.xml new file mode 100644 index 000000000..03c77099f --- /dev/null +++ b/app/src/main/res/drawable/ic_menu_gallery.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_menu_slideshow.xml b/app/src/main/res/drawable/ic_menu_slideshow.xml new file mode 100644 index 000000000..5e9e163a5 --- /dev/null +++ b/app/src/main/res/drawable/ic_menu_slideshow.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_more.xml b/app/src/main/res/drawable/ic_more.xml new file mode 100644 index 000000000..14ff51ef8 --- /dev/null +++ b/app/src/main/res/drawable/ic_more.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_more_horiz.xml b/app/src/main/res/drawable/ic_more_horiz.xml new file mode 100644 index 000000000..8fd17f690 --- /dev/null +++ b/app/src/main/res/drawable/ic_more_horiz.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_outline_remove_red_eye_24.xml b/app/src/main/res/drawable/ic_outline_remove_red_eye_24.xml new file mode 100644 index 000000000..efe7c38ce --- /dev/null +++ b/app/src/main/res/drawable/ic_outline_remove_red_eye_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_pending.xml b/app/src/main/res/drawable/ic_pending.xml new file mode 100644 index 000000000..16213ee46 --- /dev/null +++ b/app/src/main/res/drawable/ic_pending.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_person.xml b/app/src/main/res/drawable/ic_person.xml new file mode 100644 index 000000000..c80452a5a --- /dev/null +++ b/app/src/main/res/drawable/ic_person.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_repeat.xml b/app/src/main/res/drawable/ic_repeat.xml new file mode 100644 index 000000000..fce09b20e --- /dev/null +++ b/app/src/main/res/drawable/ic_repeat.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_repeat_full.xml b/app/src/main/res/drawable/ic_repeat_full.xml new file mode 100644 index 000000000..dda5fa598 --- /dev/null +++ b/app/src/main/res/drawable/ic_repeat_full.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_reply.xml b/app/src/main/res/drawable/ic_reply.xml new file mode 100644 index 000000000..bd35ee9e7 --- /dev/null +++ b/app/src/main/res/drawable/ic_reply.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_star_outline.xml b/app/src/main/res/drawable/ic_star_outline.xml new file mode 100644 index 000000000..40bb88cc7 --- /dev/null +++ b/app/src/main/res/drawable/ic_star_outline.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_success.xml b/app/src/main/res/drawable/ic_success.xml new file mode 100644 index 000000000..56749f652 --- /dev/null +++ b/app/src/main/res/drawable/ic_success.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/logo_mastodon.xml b/app/src/main/res/drawable/logo_mastodon.xml new file mode 100644 index 000000000..a55092f94 --- /dev/null +++ b/app/src/main/res/drawable/logo_mastodon.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/logo_peertub.xml b/app/src/main/res/drawable/logo_peertub.xml new file mode 100644 index 000000000..c24a69343 --- /dev/null +++ b/app/src/main/res/drawable/logo_peertub.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/media_message_border.xml b/app/src/main/res/drawable/media_message_border.xml new file mode 100644 index 000000000..1385c9931 --- /dev/null +++ b/app/src/main/res/drawable/media_message_border.xml @@ -0,0 +1,20 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/menu_selector_dark.xml b/app/src/main/res/drawable/menu_selector_dark.xml new file mode 100644 index 000000000..f87a09243 --- /dev/null +++ b/app/src/main/res/drawable/menu_selector_dark.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/menu_selector_light.xml b/app/src/main/res/drawable/menu_selector_light.xml new file mode 100644 index 000000000..acb32c17d --- /dev/null +++ b/app/src/main/res/drawable/menu_selector_light.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/nitter.xml b/app/src/main/res/drawable/nitter.xml new file mode 100644 index 000000000..f4a7b793c --- /dev/null +++ b/app/src/main/res/drawable/nitter.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/red_border.xml b/app/src/main/res/drawable/red_border.xml new file mode 100644 index 000000000..24f709aeb --- /dev/null +++ b/app/src/main/res/drawable/red_border.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/side_nav_bar.xml b/app/src/main/res/drawable/side_nav_bar.xml new file mode 100644 index 000000000..6d81870b0 --- /dev/null +++ b/app/src/main/res/drawable/side_nav_bar.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/translation_border.xml b/app/src/main/res/drawable/translation_border.xml new file mode 100644 index 000000000..f5c573b7a --- /dev/null +++ b/app/src/main/res/drawable/translation_border.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/app/src/main/res/layout/account_field_item.xml b/app/src/main/res/layout/account_field_item.xml new file mode 100644 index 000000000..a7a46760a --- /dev/null +++ b/app/src/main/res/layout/account_field_item.xml @@ -0,0 +1,58 @@ + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_actions.xml b/app/src/main/res/layout/activity_actions.xml new file mode 100644 index 000000000..360587deb --- /dev/null +++ b/app/src/main/res/layout/activity_actions.xml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_conversation.xml b/app/src/main/res/layout/activity_conversation.xml new file mode 100644 index 000000000..d91e5c58c --- /dev/null +++ b/app/src/main/res/layout/activity_conversation.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_custom_sharing.xml b/app/src/main/res/layout/activity_custom_sharing.xml new file mode 100644 index 000000000..801d9bbac --- /dev/null +++ b/app/src/main/res/layout/activity_custom_sharing.xml @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_drafts.xml b/app/src/main/res/layout/activity_drafts.xml new file mode 100644 index 000000000..bc2bddcf8 --- /dev/null +++ b/app/src/main/res/layout/activity_drafts.xml @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_edit_profile.xml b/app/src/main/res/layout/activity_edit_profile.xml new file mode 100644 index 000000000..8df9a5889 --- /dev/null +++ b/app/src/main/res/layout/activity_edit_profile.xml @@ -0,0 +1,334 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_filters.xml b/app/src/main/res/layout/activity_filters.xml new file mode 100644 index 000000000..9525d80f2 --- /dev/null +++ b/app/src/main/res/layout/activity_filters.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_hashtag.xml b/app/src/main/res/layout/activity_hashtag.xml new file mode 100644 index 000000000..8c48596b5 --- /dev/null +++ b/app/src/main/res/layout/activity_hashtag.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_instance.xml b/app/src/main/res/layout/activity_instance.xml new file mode 100644 index 000000000..790c0c630 --- /dev/null +++ b/app/src/main/res/layout/activity_instance.xml @@ -0,0 +1,131 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_instance_profile.xml b/app/src/main/res/layout/activity_instance_profile.xml new file mode 100644 index 000000000..445dae09a --- /dev/null +++ b/app/src/main/res/layout/activity_instance_profile.xml @@ -0,0 +1,199 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_instance_social.xml b/app/src/main/res/layout/activity_instance_social.xml new file mode 100644 index 000000000..eb2e2140d --- /dev/null +++ b/app/src/main/res/layout/activity_instance_social.xml @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_list.xml b/app/src/main/res/layout/activity_list.xml new file mode 100644 index 000000000..262881a5b --- /dev/null +++ b/app/src/main/res/layout/activity_list.xml @@ -0,0 +1,33 @@ + + + + + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 000000000..6f77933e0 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_media_pager.xml b/app/src/main/res/layout/activity_media_pager.xml new file mode 100644 index 000000000..9f0a39fe8 --- /dev/null +++ b/app/src/main/res/layout/activity_media_pager.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_pagination.xml b/app/src/main/res/layout/activity_pagination.xml new file mode 100644 index 000000000..52477b014 --- /dev/null +++ b/app/src/main/res/layout/activity_pagination.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_profile.xml b/app/src/main/res/layout/activity_profile.xml new file mode 100644 index 000000000..6b0e74e2a --- /dev/null +++ b/app/src/main/res/layout/activity_profile.xml @@ -0,0 +1,551 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_proxy.xml b/app/src/main/res/layout/activity_proxy.xml new file mode 100644 index 000000000..eaac44c60 --- /dev/null +++ b/app/src/main/res/layout/activity_proxy.xml @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_reorder_tabs.xml b/app/src/main/res/layout/activity_reorder_tabs.xml new file mode 100644 index 000000000..a807eb470 --- /dev/null +++ b/app/src/main/res/layout/activity_reorder_tabs.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_report.xml b/app/src/main/res/layout/activity_report.xml new file mode 100644 index 000000000..3ac01016d --- /dev/null +++ b/app/src/main/res/layout/activity_report.xml @@ -0,0 +1,427 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_scheduled.xml b/app/src/main/res/layout/activity_scheduled.xml new file mode 100644 index 000000000..d1072abfb --- /dev/null +++ b/app/src/main/res/layout/activity_scheduled.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_search_result.xml b/app/src/main/res/layout/activity_search_result.xml new file mode 100644 index 000000000..1c168cd50 --- /dev/null +++ b/app/src/main/res/layout/activity_search_result.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_search_result_tabs.xml b/app/src/main/res/layout/activity_search_result_tabs.xml new file mode 100644 index 000000000..397c8e66b --- /dev/null +++ b/app/src/main/res/layout/activity_search_result_tabs.xml @@ -0,0 +1,38 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_settings.xml new file mode 100644 index 000000000..d3169a5bf --- /dev/null +++ b/app/src/main/res/layout/activity_settings.xml @@ -0,0 +1,146 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_status_info.xml b/app/src/main/res/layout/activity_status_info.xml new file mode 100644 index 000000000..67019446f --- /dev/null +++ b/app/src/main/res/layout/activity_status_info.xml @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_webview.xml b/app/src/main/res/layout/activity_webview.xml new file mode 100644 index 000000000..ec4a52bcd --- /dev/null +++ b/app/src/main/res/layout/activity_webview.xml @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_webview_connect.xml b/app/src/main/res/layout/activity_webview_connect.xml new file mode 100644 index 000000000..772c133d4 --- /dev/null +++ b/app/src/main/res/layout/activity_webview_connect.xml @@ -0,0 +1,40 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/compose_attachment_item.xml b/app/src/main/res/layout/compose_attachment_item.xml new file mode 100644 index 000000000..eb3ce5d6a --- /dev/null +++ b/app/src/main/res/layout/compose_attachment_item.xml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/compose_poll.xml b/app/src/main/res/layout/compose_poll.xml new file mode 100644 index 000000000..32decc30e --- /dev/null +++ b/app/src/main/res/layout/compose_poll.xml @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/compose_poll_item.xml b/app/src/main/res/layout/compose_poll_item.xml new file mode 100644 index 000000000..04b86ee9c --- /dev/null +++ b/app/src/main/res/layout/compose_poll_item.xml @@ -0,0 +1,45 @@ + + + + + + + + + + diff --git a/app/src/main/res/layout/custom_tab_instance.xml b/app/src/main/res/layout/custom_tab_instance.xml new file mode 100644 index 000000000..519ba84a6 --- /dev/null +++ b/app/src/main/res/layout/custom_tab_instance.xml @@ -0,0 +1,6 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/datetime_picker.xml b/app/src/main/res/layout/datetime_picker.xml new file mode 100644 index 000000000..43bd44b9c --- /dev/null +++ b/app/src/main/res/layout/datetime_picker.xml @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/domains_blocked.xml b/app/src/main/res/layout/domains_blocked.xml new file mode 100644 index 000000000..c139c20f2 --- /dev/null +++ b/app/src/main/res/layout/domains_blocked.xml @@ -0,0 +1,10 @@ + + diff --git a/app/src/main/res/layout/drawer_account.xml b/app/src/main/res/layout/drawer_account.xml new file mode 100644 index 000000000..482dc5da5 --- /dev/null +++ b/app/src/main/res/layout/drawer_account.xml @@ -0,0 +1,142 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/drawer_account_list.xml b/app/src/main/res/layout/drawer_account_list.xml new file mode 100644 index 000000000..25d9c5e1a --- /dev/null +++ b/app/src/main/res/layout/drawer_account_list.xml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/drawer_account_reply.xml b/app/src/main/res/layout/drawer_account_reply.xml new file mode 100644 index 000000000..8f8929307 --- /dev/null +++ b/app/src/main/res/layout/drawer_account_reply.xml @@ -0,0 +1,47 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/drawer_account_search.xml b/app/src/main/res/layout/drawer_account_search.xml new file mode 100644 index 000000000..c1c54973e --- /dev/null +++ b/app/src/main/res/layout/drawer_account_search.xml @@ -0,0 +1,49 @@ + + + + + + + + + + diff --git a/app/src/main/res/layout/drawer_checkbox.xml b/app/src/main/res/layout/drawer_checkbox.xml new file mode 100644 index 000000000..51581610e --- /dev/null +++ b/app/src/main/res/layout/drawer_checkbox.xml @@ -0,0 +1,7 @@ + + diff --git a/app/src/main/res/layout/drawer_conversation.xml b/app/src/main/res/layout/drawer_conversation.xml new file mode 100644 index 000000000..af96cc9ba --- /dev/null +++ b/app/src/main/res/layout/drawer_conversation.xml @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/drawer_emoji_picker.xml b/app/src/main/res/layout/drawer_emoji_picker.xml new file mode 100644 index 000000000..5b989cf37 --- /dev/null +++ b/app/src/main/res/layout/drawer_emoji_picker.xml @@ -0,0 +1,10 @@ + + diff --git a/app/src/main/res/layout/drawer_emoji_search.xml b/app/src/main/res/layout/drawer_emoji_search.xml new file mode 100644 index 000000000..2c1dbf60b --- /dev/null +++ b/app/src/main/res/layout/drawer_emoji_search.xml @@ -0,0 +1,42 @@ + + + + + + + + diff --git a/app/src/main/res/layout/drawer_filter.xml b/app/src/main/res/layout/drawer_filter.xml new file mode 100644 index 000000000..60f5077c0 --- /dev/null +++ b/app/src/main/res/layout/drawer_filter.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/drawer_follow.xml b/app/src/main/res/layout/drawer_follow.xml new file mode 100644 index 000000000..a27f0787b --- /dev/null +++ b/app/src/main/res/layout/drawer_follow.xml @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/drawer_identity_proofs.xml b/app/src/main/res/layout/drawer_identity_proofs.xml new file mode 100644 index 000000000..dd030493b --- /dev/null +++ b/app/src/main/res/layout/drawer_identity_proofs.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/drawer_instance_reg.xml b/app/src/main/res/layout/drawer_instance_reg.xml new file mode 100644 index 000000000..22ede49eb --- /dev/null +++ b/app/src/main/res/layout/drawer_instance_reg.xml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/drawer_list.xml b/app/src/main/res/layout/drawer_list.xml new file mode 100644 index 000000000..3ec10f03d --- /dev/null +++ b/app/src/main/res/layout/drawer_list.xml @@ -0,0 +1,14 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/drawer_reorder.xml b/app/src/main/res/layout/drawer_reorder.xml new file mode 100644 index 000000000..f45ec75a6 --- /dev/null +++ b/app/src/main/res/layout/drawer_reorder.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/drawer_status.xml b/app/src/main/res/layout/drawer_status.xml new file mode 100644 index 000000000..cd09620ad --- /dev/null +++ b/app/src/main/res/layout/drawer_status.xml @@ -0,0 +1,448 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/drawer_status_compose.xml b/app/src/main/res/layout/drawer_status_compose.xml new file mode 100644 index 000000000..c27a3d70c --- /dev/null +++ b/app/src/main/res/layout/drawer_status_compose.xml @@ -0,0 +1,372 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/drawer_status_draft.xml b/app/src/main/res/layout/drawer_status_draft.xml new file mode 100644 index 000000000..a34b17822 --- /dev/null +++ b/app/src/main/res/layout/drawer_status_draft.xml @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/drawer_status_notification.xml b/app/src/main/res/layout/drawer_status_notification.xml new file mode 100644 index 000000000..8c1baa6ea --- /dev/null +++ b/app/src/main/res/layout/drawer_status_notification.xml @@ -0,0 +1,33 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/drawer_status_report.xml b/app/src/main/res/layout/drawer_status_report.xml new file mode 100644 index 000000000..ad7d4d014 --- /dev/null +++ b/app/src/main/res/layout/drawer_status_report.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/drawer_status_scheduled.xml b/app/src/main/res/layout/drawer_status_scheduled.xml new file mode 100644 index 000000000..aa6549fe4 --- /dev/null +++ b/app/src/main/res/layout/drawer_status_scheduled.xml @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/drawer_status_simple.xml b/app/src/main/res/layout/drawer_status_simple.xml new file mode 100644 index 000000000..7812294ec --- /dev/null +++ b/app/src/main/res/layout/drawer_status_simple.xml @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/drawer_tag.xml b/app/src/main/res/layout/drawer_tag.xml new file mode 100644 index 000000000..10189c4c6 --- /dev/null +++ b/app/src/main/res/layout/drawer_tag.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/drawer_tag_search.xml b/app/src/main/res/layout/drawer_tag_search.xml new file mode 100644 index 000000000..8b9a5fad6 --- /dev/null +++ b/app/src/main/res/layout/drawer_tag_search.xml @@ -0,0 +1,30 @@ + + + + + diff --git a/app/src/main/res/layout/drawer_top_menu_item.xml b/app/src/main/res/layout/drawer_top_menu_item.xml new file mode 100644 index 000000000..1881c9365 --- /dev/null +++ b/app/src/main/res/layout/drawer_top_menu_item.xml @@ -0,0 +1,34 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_login_authenticate.xml b/app/src/main/res/layout/fragment_login_authenticate.xml new file mode 100644 index 000000000..a66836813 --- /dev/null +++ b/app/src/main/res/layout/fragment_login_authenticate.xml @@ -0,0 +1,38 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_login_join.xml b/app/src/main/res/layout/fragment_login_join.xml new file mode 100644 index 000000000..53cd1df89 --- /dev/null +++ b/app/src/main/res/layout/fragment_login_join.xml @@ -0,0 +1,160 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_login_main.xml b/app/src/main/res/layout/fragment_login_main.xml new file mode 100644 index 000000000..5fdcb8632 --- /dev/null +++ b/app/src/main/res/layout/fragment_login_main.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_login_pick_instance_mastodon.xml b/app/src/main/res/layout/fragment_login_pick_instance_mastodon.xml new file mode 100644 index 000000000..b99b0e00c --- /dev/null +++ b/app/src/main/res/layout/fragment_login_pick_instance_mastodon.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_login_register_mastodon.xml b/app/src/main/res/layout/fragment_login_register_mastodon.xml new file mode 100644 index 000000000..4d2e32cc4 --- /dev/null +++ b/app/src/main/res/layout/fragment_login_register_mastodon.xml @@ -0,0 +1,158 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_notification_container.xml b/app/src/main/res/layout/fragment_notification_container.xml new file mode 100644 index 000000000..2466b0636 --- /dev/null +++ b/app/src/main/res/layout/fragment_notification_container.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_pagination.xml b/app/src/main/res/layout/fragment_pagination.xml new file mode 100644 index 000000000..317ccff46 --- /dev/null +++ b/app/src/main/res/layout/fragment_pagination.xml @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_profile_timelines.xml b/app/src/main/res/layout/fragment_profile_timelines.xml new file mode 100644 index 000000000..225f40722 --- /dev/null +++ b/app/src/main/res/layout/fragment_profile_timelines.xml @@ -0,0 +1,34 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_scheduled.xml b/app/src/main/res/layout/fragment_scheduled.xml new file mode 100644 index 000000000..84299d675 --- /dev/null +++ b/app/src/main/res/layout/fragment_scheduled.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_slide_media.xml b/app/src/main/res/layout/fragment_slide_media.xml new file mode 100644 index 000000000..a7f1263dc --- /dev/null +++ b/app/src/main/res/layout/fragment_slide_media.xml @@ -0,0 +1,134 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/layout_media.xml b/app/src/main/res/layout/layout_media.xml new file mode 100644 index 000000000..83936687c --- /dev/null +++ b/app/src/main/res/layout/layout_media.xml @@ -0,0 +1,26 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_poll.xml b/app/src/main/res/layout/layout_poll.xml new file mode 100644 index 000000000..c423f9a06 --- /dev/null +++ b/app/src/main/res/layout/layout_poll.xml @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/layout_poll_item.xml b/app/src/main/res/layout/layout_poll_item.xml new file mode 100644 index 000000000..c630f2cee --- /dev/null +++ b/app/src/main/res/layout/layout_poll_item.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/nav_header_main.xml b/app/src/main/res/layout/nav_header_main.xml new file mode 100644 index 000000000..a26eae325 --- /dev/null +++ b/app/src/main/res/layout/nav_header_main.xml @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/popup_add_filter.xml b/app/src/main/res/layout/popup_add_filter.xml new file mode 100644 index 000000000..a64709730 --- /dev/null +++ b/app/src/main/res/layout/popup_add_filter.xml @@ -0,0 +1,168 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/popup_add_list.xml b/app/src/main/res/layout/popup_add_list.xml new file mode 100644 index 000000000..8bcf2ad45 --- /dev/null +++ b/app/src/main/res/layout/popup_add_list.xml @@ -0,0 +1,17 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/popup_cache.xml b/app/src/main/res/layout/popup_cache.xml new file mode 100644 index 000000000..20b8a87a3 --- /dev/null +++ b/app/src/main/res/layout/popup_cache.xml @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/popup_contact.xml b/app/src/main/res/layout/popup_contact.xml new file mode 100644 index 000000000..b44401646 --- /dev/null +++ b/app/src/main/res/layout/popup_contact.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/layout/popup_manage_accounts_list.xml b/app/src/main/res/layout/popup_manage_accounts_list.xml new file mode 100644 index 000000000..9610980aa --- /dev/null +++ b/app/src/main/res/layout/popup_manage_accounts_list.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/popup_media_description.xml b/app/src/main/res/layout/popup_media_description.xml new file mode 100644 index 000000000..407374d0c --- /dev/null +++ b/app/src/main/res/layout/popup_media_description.xml @@ -0,0 +1,40 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/popup_notification_settings.xml b/app/src/main/res/layout/popup_notification_settings.xml new file mode 100644 index 000000000..94b6f8010 --- /dev/null +++ b/app/src/main/res/layout/popup_notification_settings.xml @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/popup_record.xml b/app/src/main/res/layout/popup_record.xml new file mode 100644 index 000000000..cabb892b6 --- /dev/null +++ b/app/src/main/res/layout/popup_record.xml @@ -0,0 +1,46 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/popup_search_instance.xml b/app/src/main/res/layout/popup_search_instance.xml new file mode 100644 index 000000000..e255b222c --- /dev/null +++ b/app/src/main/res/layout/popup_search_instance.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/popup_status_theme.xml b/app/src/main/res/layout/popup_status_theme.xml new file mode 100644 index 000000000..a3fb6a7c6 --- /dev/null +++ b/app/src/main/res/layout/popup_status_theme.xml @@ -0,0 +1,450 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/preference_time.xml b/app/src/main/res/layout/preference_time.xml new file mode 100644 index 000000000..4042eded2 --- /dev/null +++ b/app/src/main/res/layout/preference_time.xml @@ -0,0 +1,7 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/simple_bar.xml b/app/src/main/res/layout/simple_bar.xml new file mode 100644 index 000000000..f00000c8d --- /dev/null +++ b/app/src/main/res/layout/simple_bar.xml @@ -0,0 +1,54 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/tags_all.xml b/app/src/main/res/layout/tags_all.xml new file mode 100644 index 000000000..7171fa331 --- /dev/null +++ b/app/src/main/res/layout/tags_all.xml @@ -0,0 +1,15 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/tags_any.xml b/app/src/main/res/layout/tags_any.xml new file mode 100644 index 000000000..f589b953e --- /dev/null +++ b/app/src/main/res/layout/tags_any.xml @@ -0,0 +1,15 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/tags_instance.xml b/app/src/main/res/layout/tags_instance.xml new file mode 100644 index 000000000..468d0ecf1 --- /dev/null +++ b/app/src/main/res/layout/tags_instance.xml @@ -0,0 +1,15 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/tags_name.xml b/app/src/main/res/layout/tags_name.xml new file mode 100644 index 000000000..9bddf9505 --- /dev/null +++ b/app/src/main/res/layout/tags_name.xml @@ -0,0 +1,15 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/thumbnail.xml b/app/src/main/res/layout/thumbnail.xml new file mode 100644 index 000000000..8b97390b1 --- /dev/null +++ b/app/src/main/res/layout/thumbnail.xml @@ -0,0 +1,40 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/webview_actionbar.xml b/app/src/main/res/layout/webview_actionbar.xml new file mode 100644 index 000000000..c400e563b --- /dev/null +++ b/app/src/main/res/layout/webview_actionbar.xml @@ -0,0 +1,40 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/activity_main_drawer.xml b/app/src/main/res/menu/activity_main_drawer.xml new file mode 100644 index 000000000..9767f5c45 --- /dev/null +++ b/app/src/main/res/menu/activity_main_drawer.xml @@ -0,0 +1,40 @@ + +

+ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/activity_profile.xml b/app/src/main/res/menu/activity_profile.xml new file mode 100644 index 000000000..0b1275d6e --- /dev/null +++ b/app/src/main/res/menu/activity_profile.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/menu/bottom_nav_menu.xml b/app/src/main/res/menu/bottom_nav_menu.xml new file mode 100644 index 000000000..3fda0121c --- /dev/null +++ b/app/src/main/res/menu/bottom_nav_menu.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/main.xml b/app/src/main/res/menu/main.xml new file mode 100644 index 000000000..0642c09c7 --- /dev/null +++ b/app/src/main/res/menu/main.xml @@ -0,0 +1,20 @@ + + + + + + + diff --git a/app/src/main/res/menu/main_login.xml b/app/src/main/res/menu/main_login.xml new file mode 100644 index 000000000..62e5a8518 --- /dev/null +++ b/app/src/main/res/menu/main_login.xml @@ -0,0 +1,30 @@ + + + + + + + + + diff --git a/app/src/main/res/menu/main_webview.xml b/app/src/main/res/menu/main_webview.xml new file mode 100644 index 000000000..2b18c42bb --- /dev/null +++ b/app/src/main/res/menu/main_webview.xml @@ -0,0 +1,14 @@ + + + + + diff --git a/app/src/main/res/menu/menu_accounts.xml b/app/src/main/res/menu/menu_accounts.xml new file mode 100644 index 000000000..2c23a2a96 --- /dev/null +++ b/app/src/main/res/menu/menu_accounts.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/app/src/main/res/menu/menu_compose.xml b/app/src/main/res/menu/menu_compose.xml new file mode 100644 index 000000000..8fc21461c --- /dev/null +++ b/app/src/main/res/menu/menu_compose.xml @@ -0,0 +1,24 @@ + + + + + + + diff --git a/app/src/main/res/menu/menu_context.xml b/app/src/main/res/menu/menu_context.xml new file mode 100644 index 000000000..31b56e664 --- /dev/null +++ b/app/src/main/res/menu/menu_context.xml @@ -0,0 +1,14 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/menu_draft.xml b/app/src/main/res/menu/menu_draft.xml new file mode 100644 index 000000000..da3f1d21e --- /dev/null +++ b/app/src/main/res/menu/menu_draft.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/menu_edit_profile.xml b/app/src/main/res/menu/menu_edit_profile.xml new file mode 100644 index 000000000..52162f737 --- /dev/null +++ b/app/src/main/res/menu/menu_edit_profile.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/menu_list.xml b/app/src/main/res/menu/menu_list.xml new file mode 100644 index 000000000..7291001ca --- /dev/null +++ b/app/src/main/res/menu/menu_list.xml @@ -0,0 +1,14 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/menu_main_list.xml b/app/src/main/res/menu/menu_main_list.xml new file mode 100644 index 000000000..01b82f787 --- /dev/null +++ b/app/src/main/res/menu/menu_main_list.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/menu_media.xml b/app/src/main/res/menu/menu_media.xml new file mode 100644 index 000000000..a6ad64d16 --- /dev/null +++ b/app/src/main/res/menu/menu_media.xml @@ -0,0 +1,14 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/menu_reorder.xml b/app/src/main/res/menu/menu_reorder.xml new file mode 100644 index 000000000..658195518 --- /dev/null +++ b/app/src/main/res/menu/menu_reorder.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/menu_search.xml b/app/src/main/res/menu/menu_search.xml new file mode 100644 index 000000000..024b5513a --- /dev/null +++ b/app/src/main/res/menu/menu_search.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/option_tag_timeline.xml b/app/src/main/res/menu/option_tag_timeline.xml new file mode 100644 index 000000000..d08fde7e5 --- /dev/null +++ b/app/src/main/res/menu/option_tag_timeline.xml @@ -0,0 +1,38 @@ + + + + + + + + + + diff --git a/app/src/main/res/menu/option_toot.xml b/app/src/main/res/menu/option_toot.xml new file mode 100644 index 000000000..ccece807c --- /dev/null +++ b/app/src/main/res/menu/option_toot.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 000000000..ac94b34f5 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 000000000..ac94b34f5 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..4590ef3b2 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 000000000..9aea7d724 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..902c8d5c3 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 000000000..d5d6b5246 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..540bd3f79 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 000000000..43116b15f Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..5cbe432dc Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 000000000..d00144848 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..0c8ac2cfe Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 000000000..7e1102a9c Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/raw/boop.mp3 b/app/src/main/res/raw/boop.mp3 new file mode 100644 index 000000000..02a035d91 Binary files /dev/null and b/app/src/main/res/raw/boop.mp3 differ diff --git a/app/src/main/res/transition/anim.xml b/app/src/main/res/transition/anim.xml new file mode 100644 index 000000000..fbd1b8d50 --- /dev/null +++ b/app/src/main/res/transition/anim.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/transition/change_image_transform.xml b/app/src/main/res/transition/change_image_transform.xml new file mode 100644 index 000000000..6897e1606 --- /dev/null +++ b/app/src/main/res/transition/change_image_transform.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml new file mode 100644 index 000000000..b8d284b46 --- /dev/null +++ b/app/src/main/res/values-ar/strings.xml @@ -0,0 +1,1173 @@ + + + افتح القائمة + اغلق القائمة + عن التطبيق + عن مثيل الخادم + الخصوصية + الذاكرة المؤقتة + تسجيل الخروج + تسجيل الدخول + + إغلاق + موافق + لا + إلغاء + تنزيل + تنزيل %1$s + تم حفظ الصورة + ملف: %1$s + كلمة السر + البريد الإلكتروني + الحسابات + التبويقات + الوسوم + حفظ + استعادة + لم يتم العثور على أية نتيجة! + مثيل الخادم + مثيل الخادم: mastodon.social + يعمل الآن بحساب %1$s + إضافة حساب + تم نسخ محتوى التبويق إلى الحافظة + تم نسخ عنوان رابط التبويق إلى الحافظة + تعديل + حدد صورة… + تنظيف + آلة التصوير + حذف الكل + ترجم هذا التبويق. + برمجة + حجم الخط والأيقونات + تعديل حجم الخط الحالي: + تعديل الحجم الحالي للأيقونات: + التالي + العودة + فتح بواسطة + موافق + الوسائط + شارك بواسطة + تمت مشاركته عبر فيديلاب + الردود + اسم المستخدم + المسودات + المفضلة + متابِعون جدد + الإشارات + الترقيات + عرض الترقيات + عرض الردود + افتح في المتصفح + ترجمة + الرجاء الانتظار بضع ثوان قبل اتخاذ هذا الإجراء. + + الرئيسية + الخيط المحلي + الخيط المُوحَّد + الخيارات + المفضلة + التواصل + الحسابات المكتومة + الحسابات المحظورة + الإشعارات + طلبات المتابعة + الإعدادات + حذف حساب + هل تود إزالة حساب %1$s من التطبيق ؟ + إرسال بريد إلكتروني + اضغط على رابط المسار لتعديله + فشل! + التبويقات المبرمجة + قد لا تعكِس المعلومات أدناه كامل الملف الشخصي للمستخدِم. + إضافة إيموجو + ليس بإمكان التطبيق حاليًا حصاد الإيموجي الخاصة. + إشعارات منبثقة + هل أنت متأكد من أنك تريد تسجيل الخروج؟ + هل أنت متأكد من أنك تريد تسجيل الخروج @%1$s@%2$s؟ + + لا توجد هناك أية تبويقات لعرضها + لا توجد قصص لعرضها + القصص + رقاه %1$s + هل تود إضافة هذا التبويق إلى مفضلاتك ؟ + هل تود إزالة هذا التبويق من مفضلاتك ؟ + هل تود ترقية هذا التبويق؟ + هل تود إلغاء ترقية هذا التبويق؟ + هل تود تدبيس هذا التبويق؟ + هل تود فك تدبيس هذا التبويق ؟ + كتم + حظر + الإبلاغ + حذف + نسخ + شارك + أذكر + فترة الكتم + حذف وإعادة الصياغة + + هل تود كتم هذا الحساب؟ + هل تود حجب هذا الحساب؟ + هل تود الإبلاغ عن هذا التبويق؟ + هل تريد حظر هذا النطاق؟ + هل تود إلغاء كتم هذا الحساب؟ + أتريد إلغاء حظر هذا الحساب؟ + + + إشعار + صامت + + + هل تود حذف هذا التبويق؟ + هل تريد حقا حذف و إعادة صياغة هذا التبويق؟ + + الفواصل المرجعية + إضافة إلى الفواصل المرجعية + حذف الفاصلة المرجعية + لا توجد أية فواصل مرجعية للعرض + تم إضافة المنشور إلى الفواصل المرجعية! + تمت إزالة المنشور مِن الفواصل المرجعية! + + %d ثا + %d د + %d سا + %d يو + + %d ثواني + ثانية واحدة + ثانيتَيْن + %d ثوانٍ + %d ثوانٍ + %d ثوانٍ + + + صفر دقيقة + دقيقة واحدة + دقيقتَيْن + %d دقائق + %d دقائق + %d دقائق + + + صفر ساعة + ساعة واحدة + ساعتان + %d ساعات + %d ساعات + %d ساعات + + + لا يوم + يوم واحد + يومَيْن + %d أيام + %d أيام + %d أيام + + + تحذير + فيم تفكر؟ + بوِّق! + كويت! + ت.م + تحرير تبويق + الرد على تبويق + كتابة كويت + رد على كويت + تحديد وسائط + طرأ هناك خطأ أثناء اختيار الوسائط! + هل تود حذف هذه الصورة؟ + إن تبويقك فارغ! + طريقة نشر التبويق + الأسلوب الافتراضي لنشر التبويقات: + تم إرسال التبويق! + بصدد الرد على هذا التبويق: + محتوى حساس؟ + + أنشر على الخيوط العمومية + لا تقم بالنشر على الخيوط العمومية + أنشر إلى متابعيك فقط + أنشر إلى المستخدمين المشار إليهم فقط + + لا توجد مسودات! + اختر تبويقا + اختر حسابا + قم باختيار بعض من الحسابات + هل تود حذف المسودة؟ + اضغط على الزر لعرض التبويق الأصلي + وصف للمعاقين بصريا + + لا يوجد وصف! + + الإصدار %1$s + المُطوّر: + الرخصة: + GNU GPL V3 + الشفرة المصدرية: + ترجمة التبويقات: + البحث عن مثيلات الخوادم: + مصمم الأيقونات: + + محادثة + + لا توجد حسابات للعرض + ليس هناك طلبات بالمتابعة + التبويقات \n %1$s + المُتابَعون \n %1$s + المتابِعون \n %1$s + تم تثبيت \n%d + تفويض + رفض + + ليس عندك أية تبويقات مبرمَجة للعرض! + قم بتحرير تبويق ثم اختر برمَجة على القائمة العلوية. + هل تود حذف التبويق المُبرمَج؟ + الوسائط: %d + تمت برمجة التبويق بنجاح! + يجب أن يكون تاريخ البرمجة أكبر من الساعة الحالية! + وضع توفير الطاقة على البطارية مشغَّل! مِن المحتمل ألا يعمل كالمعتاد. + + ينبغي أن تكون مدة الكتم أطول مِن دقيقة واحدة. + %1$s مكتوم إلى غاية %2$s.\n بإمكانك إلغاء كتم حساب المستخدم على صفحة ملفه الشخصي. + %1$s مكتوم إلى غاية %2$s.\n بإمكانك إلغاء كتم حساب المستخدم بالنقر هنا. + + لا توجد هناك إخطارات للعرض + أشار إليك + أنشىء رسالة جديدة + رقى تبويقك + أُعجِب بتبويقك + يتابعك + طلب متابَعتك + + و اشعار آخَر + و إشعار آخر + و إشعارين + و %d اشعارات + و %d اشعارات + و %d إشعارات أخرى + + + %d إعجاب + %d إعجاب + %d إعجابان + %d إعجابات + %d إعجابات + %d إعجابات + + هل تريد حذف الإشعار؟ + هل تود حذف كافة الإشعارات؟ + تم حذف الإشعار بنجاح! + تم حذف جميع الإشعارات بنجاح! + + يتابِع + المتابِعون + التبويقات المُثبَّتة + + تعذرت عملية جلب معرف العميل! + تعذر الاتصال بنطاق مثيل الخادم! + لا يوجد اتصال بشبكة الإنترنت! + تم حجب الحساب! + لقد تم إلغاء حظر هذا الحساب! + تم كتم الحساب! + لقد تم إلغاء الكتم عن هذا الحساب! + تمت متابعة الحساب! + لم تعد تتابع هذا الحساب! + تمت ترقية التبويق! + تمت عملية إلغاء ترقية هذا التبويق! + تمت إضافة التبويق إلى مفضلاتك! + تمت إزالة التبويق من مفضلاتك! + تم الإبلاغ عن التبويق! + تم حذف التبويق بنجاح! + تم تثبيت التبويق بنجاح! + تم إلغاء تثبيت التبويق بنجاح! + عذراً ! حدث خطأ! + طرأ هناك خطأ! لم يقم مثيل الخادم بإعادة رمز المصادقة! + يبدو أنّ اسم نطاق مثيل الخادوم غير صالح! + طرأ هناك خطأ أثناء التحوّل مِن حساب إلى آخر! + وقع خطأ أثناء عملية البحث! + تم حفظ بيانات الملف الشخصي بنجاح! + لا يمكن اتخاذ أي إجراء + تم حفظ الصورة بنجاح! + وقع خطاء أثناء عملية الترجمة! + لقد تم تعطيل الترجمات في الإعدادات + تم حِفظ المُسوَدَّة! + هل أنت متأكد من أنّ مثيل الخادوم هذا يسمح بهذا الكمّ من الأحرف؟ عادةً ما تكون القيمة تقرب من 500 حرف. + لقد تم تغيير نمط عرض التبويقات على الحساب %1$s + + عدد التبويقات التي يتم تحميلها كل مرة + دائما + واي فاي + أُطلُب + تحميل الوسائط + تحميل الصور + عرض المزيد… + اعرض أقل… + محتوى حساس + تعطيل الصور الرمزية المتحركة + المسار: + حفظ تلقائي للمسودات + إضافة روابط الوسائط في التبويقات + تنبيهي عندما يتبعني أحدهم + تنبيهي عندما يقوم أحدهم بترقية منشوري + إخطاري عندما يُعجَب أحدهم بأحد منشوراتي + إخطاري عندما يُشار إليّ + أرسل إشعاراً عند انتهاء استطلاع الرأي + إشعار بالمشاركات الجديدة + عرض مربع حوار للتأكيد قبل ترقية أي تبويق + عرض مربع حوار للتأكيد قبل إضافة أي تبويق إلى المفضلة + تفعيل الإخطار في وضع الواي فاي فقط + هل تود الإشعار؟ + كتم الاشعارات + مهلة عرض الـ NSFW (بالثواني، 0 يعني مُعطَّل) + مهلة عرض وصف الوسائط (بالثواني، استخدم 0 للإيقاف) + تعديل الملف الشخصي + المشاركة المخصصة + رابط المشاركة المعدل… + السيرة الذاتية… + تأمين الحساب + حفظ التغييرات + اختيار الصورة الرأسية + تكافؤ معاينة الصور + تقسيم التبويقات على شكل ردود عندما تفوق عدد الحروف: + لقد بلغت حد الـ 160 حرف المسموح به! + لقد بلغت حد الـ 30 حرف المسموح به! + بين + و + يجب أن تكون المدة الزمنية أكبر مِن %1$s + يجب أن تكون المدة الزمنية أقصر مِن %1$s + من + إلى + استخدم المتصفح المُدمَج في التطبيق + الألسنة الخاصة + تفعيل الجافاسكريبت + توسيع نافذة التحذير عن المحتوى تلقائيًا + تقبُّل كعكات الطرف الثالث + مفتاح API، يمكن تركه فارغا لاستخدام ياندكس + + مظلمة + فاتحة + سوداء + + اختيار لون الإشعار الضوئي: + + أزرق + سماوي + أرجواني + أخضر + أحمر + أصفر + أبيض + + اتبع + إلغاء الحظر + كتم + إلغاء الكتم + تم إرسال الطلب + يتابعك + البحث + حروف كبيرة في أول الجملة عند الرد + تعديل حجم الصور + تغيير حجم الفيديوهات + + الإشعارات المدفوعة + الرجاء التأكّد مِن الإشعارات المدفوعة التي تودّ تلقيها. + بإمكانك تفعيل أو تعطيل هذه الإشعارات لاحقًا عبر الإعدادات (في تبويب الإخطارات). + + + مسح ذاكرة التخزين المؤقت + هناك %1$s من البيانات في ذاكرة التخزين المؤقتة.\n\nهل تودّ إزالتها ؟ + ميغابايت + تم تنظيف ذاكرة التخزين المؤقتة و تفريغ %1$s + + العنوان + العنوان… + الوصف + الكلمات المفتاحية + الكلمات المفتاحية… + + مزامنة + عامل التصفية + تبويقاتك + إشعاراتك + العمومية + غير المدرجة + الخاصة + المباشرة + بعض الكلمات المفتاحية… + إظهار الوسائط + إظهار المدبسة + لا توجد أي نتيجة تتطابق طلب البحث! + النسخ الاحتياطي لتبويقات حساب %1$s + تمت عملية استرجاع %1$s تبويقات جديدة + %1$s إشعارات جديدة تم استيرادها + + التاريخ تنازليًا + التاريخ تصاعديًا + + + لا + فقط + كلاهما + + لم يتم العثور على أي تبويق في قاعدة البيانات. يرجى استخدام زر المزامنة المتواجد في القائمة لجلبها. + + البيانات المسجلة + لا يقوم التطبيق إلا بحفظ معلومات عن الحسابات في الجهاز. + تبقى هذه البايانات سرية و لا تُستخدَم إلا لتشغيل التطبيق. + إنّ حذف التطبيق يؤدي إلى إزالة هذه البيانات.\n + ⚠ لا يقوم التطبيق بحفظ إسم المستخدم و الكلمات السرية. يقوم التطبيق باستخدمها و فقط عند الإتصال الآمن و المصادقة عبر (SSL) مع مثيل خادوم. + + التصريحات: + - ACCESS_NETWORK_STATE: تستخدم للكشف عن إذا كان الجهاز متصلا بشبكة واي فاي.\n + - INTERNET: تستخدم لطلبات الاستعلامات على مثيلات الخوادم.\n + - WRITE_EXTERNAL_STORAGE: تستخدم لتخزين الوسائط أو نقل التطبيق على بطاقة SD.\n + - READ_EXTERNAL_STORAGE: تستخدم لإضافة الوسائط في التبويقات.\n + - BOOT_COMPLETED: يستخدم لبدء تشغيل خدمة الإشعار.\n + - WAKE_LOCK: تستخدم في خدمة الإشعارات. + + تصريحات خدمة برمجة التطبيقات API: + - القراءة: قراءة البيانات.\n + - الكتابة: نشر المنشورات و رفع الصور الخاصة بالمنشورات.\n + - المتابعة: المتابعة و إلغاء المتابعة و الحظر و إلغاء الحظر.\n\n + ⚠ يقوم التطبيق بهذه الإجراءات عند طلب المُستخدِم لاستعمالها. + + التعقب و المكتبات + إنّ التطبيق لا يستخدم أدوات للتعقّب (لقياس الجمهور، للإبلاغ عن الأخطاء، إلخ.) ولا يحتوي على أية إعلانات تجارية.\n\n + تم التقليل من استخدام المكتبات: \n + - Glide: لإدارة الوسائط\n + - Android-Job: لإدارة الخدمات\n + - PhotoView: لإدارة الصور\n + + ترجمة التبويقات + يوفر التطبيق امكانية ترجمة التبويقات باستخدام لغة الجهاز وواجهة برمجة التطبيقات Yandex API.\n + لدى ياندكس سياستهم الخاصة بشأن الخصوصية و يمكن العثور عليها هنا: https://yandex.ru/legal/confidential/?lang=en + + الشكر لـ : + التصفية بواسطة العبارات المنطقية + البحث + حذف + تحميل المزيد من التبويقات… + + القوائم + هل تود فعلا حذف هذه القائمة ؟ + لا تزال هذه القائمة فارغة إلى حد الآن. سوف تظهر هنا تبويقات الأعضاء المنتمين إليها حالَما يقومون بالنشر. + إضافة إلى القائمة + إنشاء قائمة جديدة + حذف القائمة + تعديل القائمة + عنوان جديد للقائمة + تمت إضافة الحساب إلى القائمة! + لا توجد لديك أية قوائم إلى حدّ الآن! + + %1$s انتقلَ إلى %2$s + هل المصادقة لا تعمل؟ + b>وهنا بعض الاختبارات التي قد تساعدكم:</b>\n\n + - تحققوا من عدم وجود الأخطاء الإملائية في اسم مثيل الخادم\n\n + - تحققوا مِن أنّ مثيل الخادم يشتغل\n\n + - إذا كنتم تستخدمون المصادقة بخطوتين (2FA) الرجاء استخدام الرابط في الأسفل (يجب إدخال اسم السيرفر أولا)\n\n + - يمكنكم أيضا استخدام هذا الرابط بدون استخدام 2FA\n\n + - إذا كان لا يزال لا يعمل ، يرجى إرسال تقرير على FramaGit إلى https://framagit.org/tom79/fedilab/issues + + تم تحميل الوسائط. اضغط هنا للعرض. + يمكن أن تستغرق هذه العملية وقتًا طويلًا. سوف نقوم بإشعارك عند تتمتها. + جارٍ التشغيل، الرجاء الانتظار… + تصدير المنشورات + تصدير بيانات %1$s + تم تصدير %1$s تبويقات من أصل %2$s. + طرأ هناك خطأ أثناء تصدير بيانات %1$s + حدث خطأ ما عند تصدير البيانات! + حدث خطأ ما عند استيراد البيانات! + + البروكسي + تشغيل البروكسي؟ + المضيف + المنفذ + اسم المستخدم + كلمة السر + إضافة التفاصيل عند مشاركة تبويق ما + ادعم التطبيق على ليبيراباي + هناك خطأ في العبارة المنطقية! + لم يتم العثور على أي خيط زمني في مثيل الخادم هذا! + هل تريد إزالة مثيل الخادم هذا؟ + ترجِم إلى + اتبع مثيل الخادم + إنّك مُتابِع لمثيل الخادم هذا! + تمت مُتابَعة مثيل الخادم! + الشراكات + معلومات + إخفاء ترقيات %s + أوصِ به على صفحتك + إظهار ترقيات %s + إلغاء التوصية مِن صفحتك + تمت توصية الحساب على صفحتك الشخصية + تم إلغاء توصية هذا الحساب مِن صفحتك + سيتم إظهار الترقيات الآن! + تم إخفاء الترقيات الآن! + رسالة مباشِرة + عوامل التصفية + ليس هناك أي عامل تصفية بعدُ. يمكنك إنشاء واحد بالنقر على زر \"+\". + كلمة مفتاحية أو عبارة + الخط الزمني الرئيسي + الخطوط الزمنية العمومية + الإشعارات + المحادثات + سوف يتم العثور عليه بغض النظر عن حالة الأحرف في النص أو حتى و إن كان فيه تحذير عن المحتوى + حذف بدلًا مِن الإخفاء + سوف تختفي التبويقات المُصفّاة إلى الأبد، حتى و إن تم حذف عامل التصفية لاحقًا + إذا كانت الكلمة أو الجملة مكونة من الأرقام والحروف فقط سوف يتم تطبيقها فقط عند مطابقة الكلمة ككل + الكلمة كلها + سياق عامل التصفية + مجالات تطبيق عامل التصفية + تنتهي صلاحيته بعد + متأكد مِن حذف عامل التصفية؟ + تحديث عامل التصفية + إنشاء عامل تصفية + حسابات للمتابَعة + حاليًا ليس هناك أي حساب في القائمة! + اتبع + اختيار الكلّ + إلغاء اختيار الكلّ + %s مُتابَع الآن! + جارٍ إنشاء القائمة %s + إضافة الحسابات إلى القائمة + تمت عملية إضافة الحسابات إلى القائمة + إضافة الحسابات إلى القائمة + لم تقم بعد بإنشاء قائمة. اضغط على زر \"+\" لإنشاء قائمة. + حسابات للمُتابَعة + جذع API + لا يمكن متابعة الحساب·ات + جلب الحسابات عن بُعد + توسيع الوسائط المخفية تلقائيًا + مُتابِع جديد + ترقية جديدة + مفضلة جديدة + إشارة جديدة + انتهى استطلاع الرأي + تبويق جديد + النسخ الاحتياطي للتبويقات + مشاركات جديدة + تنزيل الوسائط + تغيير الإشعار الصوتي + اختيار نغمة + تفعيل الفترة الزمنية + الفيديوهات التوضيحية + جارٍ جلب المنشور عن بُعد! + ليس هناك أية نطاقات محظورة! + إلغاء حظر النطاق + أمتأكد أنك تريد إلغاء الحظر عن %s؟ + أمتأكد أنك تريد حظر %s؟\n\n سوف لن تَرَ أيا من محتويات هذا النطاق على أي خيط زمني عام كان أو في إشعاراتك. سيتم إزالة متابِعيك مِن هذا النطاق. + النطاقات المحظورة + حظر النطاق + تم حظر النطاق + لقد تم إلغاء حظر هذا النطاق! + جارٍ جلب المنشور عن بُعد + تعليق + مثيل خادم بيرتيوب + كن الأول في التعليق على هذا الفيديو بالضغط على الزر الأيمن في الأعلى! + %s مشاهدات + المدة: %s + إضافة مثيل خادم + إنّ التعليقات على هذا الفيديو مُعطّلة! + اختيار الجودة + مفضلات بيرتيوب + تم إضافة الفيديو إلى الفواصل المرجعية! + تم إزالة الفيديو من الفواصل المرجعية! + لا توجد أية فيديوهات بيرتيوب في مفضلاتك! + قناة + الفيديوهات + القنوات + استخدم إيموجي وان + معلومات + عرض المعاينات في كافة التبويقات + مصمم واجهة المستخدم الجديدة + عرض معاينات الفيديوهات + تم نسخ معرف الحساب إلى الحافظة! + تغيير اللغة + اللغة الافتراضية + تقسيم التبويقات الطويلة + اقتطاع التبويقات إلى \'س\' خطوط. الصفر يعني معطَّل. + عرض المزيد + اعرض أقل + إدارة الوسوم + الوسم موجود مِن قَبل! + تم حفظ الوسم! + تم تعديل الوسم بنجاح! + تم حذف الوسم! + برمجة الترقية + تمت برمجة الترقية! + ليس هناك أية برمجة للترقية للعرض! + برمجة ترقية.]]> + الخيط الفني + فتح القائمة + العودة + شعار التطبيق + الصورة الشخصية + رأسية الصفحة الشخصية + الاتصال بالمشرف على مثيل الخادوم + إضافة + شعار ماستوهوست + منتقي الإيموجي + تحديث + توسيع المحادثة + إزالة حساب + حذف النطاق المحظور + منتقي للإيموجي مخصص + تشغيل الفيديو + تبويق جديد + صورة البطاقة + إخفاء الوسائط + أيقونة المفضلة + إضافة وصف على الصور (للمعاقين بصريا) + + أبدًا + 30 دقيقة + ساعة واحدة + 6 ساعات + 12 ساعة + يوم واحد + أسبوع واحد + + في هذا الحقل تحتاج إلى كتابة اسم المضيف.\nعلى سبيل المثال ، إن أنشأتم حسابكم على https://mastodon.social\nتكفي كتابة mastodon.social (دون https://)\n + يمكنكم الشروع في كتابة الحروف الأولى وستُقترَحُ عليكم أسماء.\n\n + ⚠ زِرّ \"تسجيل الدخول\" سوف يعمل فقط إذا كان الخادم شغّالاً واسمه صالح! + + تفاصيل أكثر + + اللغات + الوسائط فقط + عرض NSFW + ترجمات كراودين + المشرف على الترجمات + ترجمة التطبيق + عن كراودين + بوت + مثيل خادم بيكسل فد + مثيل خادم ماستدون + إحدى هذه الكلمات + كل هذه الكلمات + لا شيء من هذه + أي من هذه الكلمات (مفصولة بمسافة) + كافة هذه الكلمات (مفصولة بمسافة) + أضف بعض الكلمات للتصفية (مفصولة بمسافة) + تغيير اسم العمود + مثيل خادم ميسكي + ليس هناك أي تطبيق مثبّت على جهازك يدعم هذا الرابط. + الاشتراكات + نظرة عامة + الشائعة + تم إضافتها حديثًا + المحلية + إرسال + رد + حذف التعليق + هل أنت متأكد من أنك تود حذف هذا التعليق؟ + فيديو تملأ الشاشة + أسلوب عرض الفيديوهات + اختر الملف المراد ارساله + فيديوهاتي + العنوان + الرخصة + الفئة + اللغة + هذه الفيديو تحتوي على محتوى للكبار + السماح بالتعليق على الفيديو + تحديث الفيديو + الوصف + تم تحديث الفيديو! + تم إلغاء الإرسال! + تم ارسال الفيديو! + الارسال جارٍ، يُرجى الانتظار… + اضغط هنا لتعديل بيانات الفيديو. + احذف الفيديو + هل أنت متأكد أنك تود حذف هذا الفيديو؟ + عرض الفيديوهات الحساسة + لا توجد أية فيديوهات لعرضها! + اترك تعليقاً + شارك + اختر أسلوب برمجة التبويقات + عبر الجهاز + عبر الخادِم + التبويقات (السيرفر) + التبويقات (الجهاز) + تغيير + إظهار التبويقات الجديدة فوق زر \"عرض المزيد\" + الخيوط الزمنية + الواجهة + المتراسلون + %1$s على الفيديو %2$s]]> + %1$s يتابع قناتك %2$s الآن]]> + %1$s يُتابِع حسابك الآن]]> + %1$s]]> + %1$s بنجاح]]> + %1$s]]> + %1$s فيديو جديد: %2$s]]> + %1$s في القائمة السوداء]]> + %1$s من القائمة السوداء]]> + صدّر البيانات + استورد البيانات + حدّد الملف الذي تريد استيراده + حدث خطأ عند اختيار ملف النسخ الاحتياطي! + أضف تعليقًا عامًا + ارسل تعليق + لا يوجد اتصال بالإنترنت. تم تخزين رسالتك في المسودات. + نص عادي + HTML + ماركداون + الخروج مِن الحساب + الكل + ادعم التطبيق + فتح الجماعية تمكن الجماعات إلى المسارعة الجماعية ، جمع الأموال وإدارتها بشفافية. + انسخ الرابط + اتصل + عادي + مضغوط + الطرفية + اختيار طريقة العرض + تحسين موفر الأمان + تحديث نطاقات التعقب + تم تحديث قاعدة بيانات التعقب! + طلبات الـ http التي حجبها التطبيق + قائمة الطلبات المحجوبة + إرسال + تم تصدير قاعدة البيانات! + الوسوم الشائعة + تصفية الخيط الزمني باستخدام الوسوم + لا يوجد هناك وسم + إخفاء زر \"الحذف\" المتواجد في لسان الإشعارات + ارفاق صورة عند مشاركة عنوان رابط + + استطلاع رأي + استطلاعات الرأي + إنشاء استطلاع + الخيار 1 + الخيار 2 + الخيار %d + يجب أن يتوفر استطلاع الرأي على خيارين على الأقل! + تم + ينتهي في %s + تحديث استطلاع الرأي + تصويت + لقد انقضت مدة استطلاع رأي قد قُمتَ بالتصويت عليه مِن قَبل + لقد انتهت مدة استطلاع رأي قد قُمتَ بنشره + تخصيص + الفئات + الفترة الزمنية + خيارات متقدمة + عرض شارة \'new\' على التبويقات غير المقروءة + Peertube + نقل الخيط الزمني + إخفاء الخيط الزمني + إعادة ترتيب الخيوط الزمنية + تم حذف القائمة نهائيا + تم إزالة مثيل الخادم المُتابَع + تم إزالة الوسم المدبس + إلغاء + يجب الإبقاء على لسانَين ظاهرين! + إعادة ترتيب الخيوط الزمنية + الخيوط الرئيسية يمكن فقط إخفاءها! + BBCode + اعتبار الوسائط دائما كحساسة + مثيل خادم GNU + منشور مُخزَّن + نقل الوسوم في الردود + اضغط مطولاً لحفظ الوسائط + تعتيم الوسائط الحساسة + عرض الخيوط الزمنية في قائمة + عرض الخيوط الزمنية + ضع إشارة على حسابات الروبوتات في التبويقات + إدارة الوسوم + تذكّر وضعيتي على الخيط الرئيسي + التاريخ + قوائم التشغيل + الاسم العلني + ليس لديك أي قوائم تشغيل. انقر على أيقونة \"+\" لإضافة قائمة تشغيل جديدة + يجب عليك إدخال اسم علني! + القناة لازمة إن كانت قائمة التشغيل متاحة للعامة. + إنشاء قائمة تشغيل + قائمة التشغيل هذه فارغة حاليا. + إعادة + المعرض + إيموجي + ملصق + مِمْحاة + نص + فلتر + فرشاة + هل أنت متأكد من الخروج دون حفظ الصورة؟ + تجاهل + جاري الحفظ… + تم حفظ الصورة بنجاح! + فشل في حفظ الصورة + الشفافيّة + تمكين محرر الصور + إضافة خيار إلى الاستطلاع + إزالة آخر عنصر في الاستطلاع + اكتم المحادثة + ألغِ كتم المحادثة + لقد تم إلغاء الكتم عن هذه المحادثة! + المحادثة مكتومة + افتح ميزات التطبيق + فترة الكتم + اذكر الحساب + تحديث ذاكرة التخزين المؤقت + اذكر هذا المنشور + الأخبار + عام + إقليمية + فن + صحافة + للنشطاء + ألعاب + تكنولوجيا + محتوى للكبار + فروي + طعام + شعار مثيل الخادم + حدث خطأ أثناء التحقق من توفر مثيلات الخوادم! + انضم إلى ماستدون + اختر مثيل خادم من بين الفئات ، ثم اضغط على زر الاختيار. + اختر خادمًا بالضغط على زر التحقق. + %1$s مستخدم + تأكيد الكلمة السرية + أوافق على %1$s وعلى %2$s + قواعد الخادم + شروط الخدمة + إنشاء حساب + مثيل الخادم هذا يعمل بنظام الدعوات. لكي يكون باستطاعتك استخدام حسابك ، يجب على أحد مدراء الخادم مراجعة طلب انضمامك يدويا. + يرجى ملء جميع الحقول! + كلمتي المرور غير متطابقتين! + يبدو أنّ عنوان البريد الإلكتروني غير صالح! + اسم المستخدم الخاص بك سوف يكون فريدا من نوعه على %1$s + سيتم إرسال رسالة إلكترونية للتأكيد + استخدم 8 أحرف على الأقل + يجب أن تكون الكلمة السرية مكوّنة مِن 8 أحرف على الأقل + اسم المستخدم يجب أن تحتوي فقط على حروف وأرقام وسطور سفلية + تم إنشاء الحساب! + Your account has been created!\n\n + Think to validate your email within the 48 next hours.\n\n + You can now connect your account by writing %1$s in the first field and click on Connect.\n\n + Important: If your instance required validation, you will receive an email once it is validated! + + هل تريد حفظ الرسالة في المسودّات؟ + الإدارة + التقارير + لا توجد هناك أية تقارير لعرضها! + أعد توصيل الحساب + لقد فشل التطبيق في الوصول إلى ميزات الإشراف. قد تحتاج إلى إعادة توصيل الحساب قصد الحصول على التصريحات اللازمة لذلك. + لم يتم حله + عن بعد + نشط + معلّق + معطّل + تم كتمه + تم تعليقه + التصريحات + حالة البريد الإلكتروني + حالة الولوج + انضم + أحدث عنوان إيبي + تحذير + تعطيل + اكتمه + أخبر المستخدم عبر البريد الإلكتروني + تحذير مخصص + المستخدم + المُشرف + المدير + مؤكّد + غير مؤكّد + المنشورات المبلّغ عنها + الحساب + إلغاء الكتم + إلغاء التعطيل + تعليق + إلغاء التعليق + تم كتم هذا الحساب! + لقد تم إلغاء كتم الحساب! + تمّ تعليق هذا الحساب! + لم يعد هذا الحساب معلّقا! + تم تعطيل الحساب! + لم يعد هذا الحساب معطّلا! + لقد تم تحذير الحساب! + أظهر اللوح الإداري + اعرض الميزات الإدارية على المنشورات + سماح + تتم الموافقة على الحساب! + تم رفض الحساب! + تعيين لي + إلغاء التعيين + اعتبار الشكوى كمحلولة + اعتبار الشكوى كغير محلولة + محتوى فارغ! + اعرض زر ميزات Fedilab + يحتاج التطبيق إلى الوصول إلى تسجيل الصوت + رسالة صوتية + تمكين الرد السريع + لا يتمكن الحساب الذي يتم الرد عليه من قد رؤية رسالتك! + عند التعطيل سيقوم التطبيق دائما بتحميل آخر المنشورات + عند التعطيل سيتم حجب المحتوى الحساس بواسطة زر + حفظ الوسائط بالحجم الكامل بضغطة مطولة على معاينات + إضافة زر بعلامات الحذف في أعلى يمين كل العلامات والمنشورات والقوائم والمثيلات + خلال هذه الفتحة الزمنية سيقوم التطبيق بإرسال إخطارات. يمكنك عكس (أي تصميت) هذا الفتحة الزمنية باستخدام الزر الموافق. + اعرض زر فيديلاب تحت الصورة الشخصية على شكل اختصار لولوج خاصيات البرنامج. + إتاحة الرد مباشرة في الخط الزمني أدنى المنشورات + لتعطيل اقتصاص المعاينات في الخطوط الزمنية + إتاحة تشغيل الفيديوهات المدمجة في الخط الزمني + السماح بعكس طريقة قرأ المنشورات التي تظهر عند ضغط زر جَلب المزيد + تمكين دعم مجموعات الشفرات الحديثة. يصلح لأجهزة القديمة أو عند تَعَذُر الاتصال بالمثيل. + خاص بـ Peertube. مكّن هذا الوضع إن تعذر تشغيل الفيديوهات. + تمكن هذه الأوسمة من تصفية المنشورات من الحسابات. سيتوجب استخدام قائمة السياق لمعاينتهم. + إدراج سطر جديد تلقائيا بعد الذكر لتكبير الحرف الأول + تمكين صناع المحتوى من مشاركة المنشورات في RSS الخاص بهم + التحرير + عدد المحاولة الأقصى عند تحميل وسائط + إنشاء مجلد جديد هنا + أدخل اسم المجلد + الرجاء ادخل اسم مجلد صحيح + هذا المجلد موجود بالفعل.\n يرجى إدخال اسم آخر للمجلد + اختار + المجلد الافتراضي + المجلد + إنشاء مجلد + عرض رسالة بعد تمام العمليات كالدعم والتفضيل وغيرها؟ + لقد تم تصدير مثيلات الخوادم المكتومة! + إضافة مثيل خادم + تصدير مثيلات الخوادم + استيراد مثيلات خوادم + تقارير الأعطال + تفعيل تقارير الأعطال + عند التفعيل سيتم إنشاء تقرير الحادث محليا حتى تكون قادر على مشاركته. + لقد توقّفَ تطبيق Mastalab :( + يمكنكم أن ترسلوا لي عن طريق البريد الإلكتروني تقرير الحادث. وستساعدون على إصلاح ذلك:)\n\n يمكن إضافة محتوى إضافي. شكرًا لكم! + أستعمال محرر ما تشاهده هو ما تحصل + عند التفعيل سوف تستطيع تهيأت النص بسهولة. + إحصائيات + مجموع المنشورات + عدد التدعيمات + عدد المفضلات + عدد الإشارات + عدد الاشتراكات + عدد استطلاعات الرأي + عدد الردود + عدد المنشورات + منشورات + الرؤية + عدد بالوسائط + عدد بوسائط حساسة + عدد بتحذير المحتوى + تاريخ أول منشور + تاريخ آخر منشور + تاريخ أول إشعار + آخِر تاريخ للإشعار + التردد + %s منشور في اليوم + %s إشعارات في اليوم + النطاق الزمني + المجموعات + لا توجد أي مجموعة! + تعطيل الإيموجي الخاصة المتحركة + الرسوم البيانية + عرض الرسوم البيانية + التطبيق يجمع البيانات المحلية الخاصة بك، المرجو الانتظار... + نسخة احتياطية + النسخ الاحتياطي التلقائي المنشورات + إختيار خاص بكل حساب على إنفراد. سوف يقوم بفتح خدمة تتكلف بحفظ منشوراتك تلقائيا محليا في قاعدة البيانات. يُمَّكِنُ هذا من الحصول على إحصائيات و مبيانات + نسخ احتياطي تلقائي للإشعارات + إختيار خاص بكل حساب على إنفراد. سوف يقوم بفتح خدمة تتكلف بحفظ إشعاراتك تلقائيا محليا في قاعدة البيانات. يُمَّكِنُ هذا من الحصول على إحصائيات و مبيانات + بلّغ عن الحساب + ارسل دعوة + مثيل خادمك لا يسمح بإنشاء حسابات جديدة! + + صوت %d واحد + صوت واحد + %d صوت + %d أصوات + %d أصوات + %d صوتا + + + لا مصوّت + مصوّت واحد + مصوّتَيْن + %d مصوّت + %d مصوّت + %d مصوّت + + + خيار واحد + خيارات متعددة + + + 5 دقائق + 30 دقيقة + ساعة واحدة + 6 ساعات + يوم واحد + 3 أيام + 7 أيام + + + Webview + تدفق مباشر + + إن أردت الانضمام إلى مثيل خادومي \"%1$s\" ، يمكنك تنزيل Fedilab:\n\nF-Droid: %2$s\nغوغل: %3$s\n\nثم افتح الرابط أدناه بـ Fedilab وأنشئ حسابك :)\n\n%4$s + + لا يمكن لاستطلاع رأيك أن يحتوي على خيارات مكرّرة! + لكافة الحسابات + التخزين المؤقت لقاعدة البيانات + امسح التخزين المؤقت لخيطك الرئيسي + امسح التخزين المؤقت للمنشورات المخزَّنة + امسح كافة فواصلك المرجعية + الملفات في ذاكرة التخزين المؤقتة + إجمالي الإشعارات + إخفاء عناصر القائمة + الإشعارات الحية تشتغل في Fedilab + لِـ %1$s حسابات تحتوي %2$s أحداث + إشعارات حية لـ %1$s + لن يتم تعطيل الإشعارات الحية سوى لهذا الحساب. + امسح ذاكرة التخزين المؤقت عند المغادرة + سيتم مسح ذاكرة التخزين المؤقت تلقائيا (الوسائط والرسائل المخزّنة وبيانات المتصفح المدمج) عند الخروج من التطبيق. + هل تريد إلغاء متابعة هذا الحساب؟ + أظهر مربع حوار التأكيد قبل إلغاء المتابعة + استبدل Youtube بـ Invidio.us + إن Invidious واجهة أمامية بديلة لـ YouTube + أدخل مضيفك المخصص أو اتركه فارغًا لاستخدام invidio.us + استبدل تويتر بـ Nitter + إن Nitter واجهة أمامية بديلة لـ Twitter تركز على الخصوصية. + أدخل مضيفك المخصص أو اتركه فارغًا لاستخدام nitter.net + استبدال Instagram بـ Bibliogram + Bibliogram هو بديل مفتوح المصدر لـ Instagram يركز على الخصوصية. + أدخل مضيفك المخصص أو اتركه فارغًا لاستخدام bibliogram.art + استبدال Reddit بـ Libreddit + Libreddit هي جهة ريديت الأمامية البديلة المفتوحة المصدر التي تركز على الخصوصية. + أدخل مضيفك المخصص أو اتركه فارغاً لاستخدام libredd.it + استبدال روابط Medium + استبدال روابط medium.com بواجهة أمامية مفتوحة المصدر تركز على الخصوصية. + الافتراضي: scribe.rip + استبدال روابط ويكيبيديا + استبدال رابط ويكيبيديا بواجهة أمامية مفتوحة المصدر تركز على الخصوصية. + الافتراضي: wikiless.org + أخفِ شريط إشعارات Fedilab + لإخفاء الإشعارات المتبقية على شريط الحالة ، اضغط على زر أيقونة العين ثم قم بإلغاء تحديد: \"العرض على شريط الحالة\" + استخدام نظام الإشعارات المنبثق للحصول على الإشعارات في الوقت الحقيقي. + ليس هناك إشعارات حية + الإشعارات الحية + سيتم جلب الإشعارات كل 15 دقيقة. + أضف ملاحظات + ملاحظات الحساب + يسمح بضغط الصور الكبيرة إلى حجم أصغر مع الحفاظ على جودتها. + يسمح بضغط الفيديوهات مع الحفاظ على جودتها. + إنّ التطبيق يقوم بضغط الوسائط حاليا ، قد تأخذ العملية شيئا مِن الوقت… + تغيير أيقونة التطبيق + اضغط لتغيير أيقونة التطبيق + انشر + طريقة نشر المنشور + اضغط هنا لإضافة صور + صيغ الملفات المقبولة: jpeg ، png ، gif \n\n حجم الملف الأقصى: 15 MB \n\n يمكن للألبومات أن تحتوي على ما يصل إلى 4 صور أو مقاطع فيديو + حمّل وسائطًا + أضف نصًّا توضيحيا + لقد تلقى التطبيق رسالة خطأ طويلة مِن API %1$s + معاينة الرسالة + إضافة الإشارات إلى الحسابات في كل رسالة + جارٍ جلب المحادثة + ترتيب حسب + عنوان للفيديو + انظم إلى PeerTube + أبلُغ مِن العُمر 16 سنة على الأقل وأوافق على %1$s هذا الخادم + الروابط + تغيير لون الروابط (عناوين URLs، الإشارات ، الوسوم، إلخ.) في الرسائل + رأسية إعادات النشر + تغيير لون الإسم المعروض في الجزء العلوي من الرسائل + تغيير لون اسم المستخدم في الجزء العلوي من الرسائل + يغيّر لون الرأسية الخاصة بإعادة النشر + المنشورات + لون خلفية المنشورات على الخيوط + صفّر الألوان + اضغط هنا لإعادة تعيين كافة إعدادات الألوان المخصصة + صفّر + الأيقونات + لون الأيقونات السفلية على الخيوط + دبّس هذا الوسم + شعار مثيل الخادم + عدّل الصفحة التعريفية + القيام بإجراء + الترجمة + معاينة الصورة + لون النص + تغيير لون النص في البقع + احفظ التعديلات + تحتاج إلى إعادة تشغيل التطبيق لتفعيل التغييرات + إعادة التشغيل + استخدم قالبا مخصصا + السماح بتجاوز ألوان الموضوع المحدد أعلاه + المظهر + احتفظ به أولًا + تم تصدير القالب + تم تصدير السمة بنجاح الى CSV + تطبيق اللون الأساسي على شريط الحالة + لون شريط الحالة + استرجاع القالب الإفتراضي + استيراد قالب + اضغط هنا لاستيراد سمة من نسخة سابقة + تصدير قالب + اضغط هنا لتصدير قالب السمة الحالي + حدث خطأ أثناء اختيار ملف قالب السمة + منتقي القوالب + حدد سمة مثبتة مسبقاً + القوالب + تطبيق اللون الأساسي على شريط التنقل + لون شريط التصفّح + اللون الأساسي لمحتوى التطبيق. + لون الخلفية + قم بتحديد أجزاء من واجهة المستخدم. + الون الثانوي + يتم عرضها بشكر متكرر عبر التطبيق الخاص بك. + اللون الأساسي + صدّر الفواصل المرجعية إلى مثيل الخادم + استيراد الفواصل المرجعية مِن مثيل الخادم + عدد المستخدمين + عدد المنشورات + عدد مثيلات الخوادم + تم حجبه + ينتهي في %s + ما الجديد في %s + يمكنك متابعة حسابي لاستقبال المستجدات + مثيل الخادم هذا غير موجود على https://instances.social + اظهر الرابط كاملًا + شارك الرابط + تم نسخ رابط العنوان إلى الحافظة + افتح في تطبيق آخر + تحقّق مِن إعادة التوجيه + هذا العنوان لا يحوّل إلى عنوان آخر + %1$s \n\nيُحوّل إلى\n\n %2$s + غيّر عميل المستخدم user agent + تعيين وكيل مستخدم مخصص أو اتركه فارغاً + يسمح بتخصيص وكيل المستخدم المستخدم لمكالمات api أو مع المتصفح المدمج. + أزل معلّمات UTM + يقوم التطبيق تلقائيا بإزالة معلمات UTM مِن عناوين المواقع قبل زيارة الرابط. + المتداوَلة + المتداوَلة الآن + %d أشخاص يتحدثون عن ذلك + حسابات تويتر (عبر Nitter) + أسماء حسابات تويتر مُفرّقة بمسافة بيضاء + دلائل الهوية + هوية مُتحقّق منها + تم التحقق منه عبر %1$s (%2$s) + احذف الإشعار + اعرض المزيد من الخيارات + إنها حكاية پيكسل فد + عند رفع الوسائط ، فإنه سيتم تلقائيا إضافتها إلى قصة Pixelfed. + تم إضافة الوسائط إلى قصتك بنجاح! + الإجراء مُعطّل + إلغاء المتابعة + حدث خطأ ما، الرجاء التحقق من مسار التحميلات في الإعدادات. + الإعلانات + ليس هناك إعلانات! + إضافة ردة فعل + استخدم المتصفح المفضل الخاص بك داخل التطبيق. قم بإلغاء تحديد هذه الميزة لفتح الروابط خارجيا. + ذاكرة التخزين المؤقت للفيديو بالـ MB، صفر تعني مِن دون ذاكرة تخزين المؤقت. + العلامات المائية + إضافة علامة مائية تلقائياً في أسفل الصور. يمكن تخصيص النص لكل حساب. + لم يتم العثور على موزعين! + تحتاج إلى موزع لتلقي إشعارات الدفع.\nستجد المزيد من التفاصيل في %1$s.\n\nيمكنك أيضا تعطيل الإشعارات في الإعدادات لتجاهل تلك الرسالة. + اختر موزعا + diff --git a/app/src/main/res/values-ber/strings.xml b/app/src/main/res/values-ber/strings.xml new file mode 100644 index 000000000..b24c55256 --- /dev/null +++ b/app/src/main/res/values-ber/strings.xml @@ -0,0 +1,1140 @@ + + + Open the menu + Close the menu + ⵖⴻⴼ + About the instance + ⵜⵉⵏⵏⵓⵜⵍⴰ + Cache + ⴼⴼⵖ + ⴽⵛⵎ + + ⵎⴸⴻⵍ + ⵢⴰⵀ + ⵓⵀⵓ + ⵙⴻⴼⵙⴻⵅ + ⴰⴳⵎ + ⴰⴳⵎ %1$s + Media saved + ⴰⴼⴰⵢⵍⵓ: %1$s + ⴰⵡⴰⵍ ⵓⴼⴼⵉⵔ + ⵉⵎⴰⵢⵍ + ⵉⵎⵉⴹⴰⵏⵏ + ⵉⵎⴰⵙⵙⵏ + Tags + ⵃⴹⵓ + Restore + No results! + Instance + Instance: mastodon.social + Now works with the account %1$s + ⵔⵏⵓ ⴰⵎⵉⴹⴰⵏ + The content of the toot has been copied to the clipboard + The URL of the toot has been copied to the clipboard + ⵙⵏⴼⵍ + ⵙⵜⵉ ⵜⴰⵡⵍⴰⴼⵜ… + Clean + ⵜⴽⴰⵎⵕⴰ + ⴽⴽⵙ ⵎⴰⵕⵕⴰ + Translate this toot. + Schedule + Text and icon sizes + Change the current text size: + Change the current icon size: + Next + Previous + ⵕⵥⵎ ⵙ + Validate + Media + ⴱⴹⵓ ⴰⴽⴷ + Shared via Fedilab + Replies + ⵉⵙⵎ ⵏ ⵓⵏⵙⵙⵎⵔⵙ + Drafts + Favourites + New followers + Mentions + Boosts + Show boosts + Show replies + Open in browser + ⵙⵙⵓⵖⵍ + Please, wait few seconds before making this action. + + ⴰⵙⵏⵓⴱⴳ + Local timeline + Federated timeline + Options + Favourites + Communication + Muted users + Blocked users + Notifications + Follow requests + ⵜⵉⵙⵖⴰⵍ + ⴽⴽⵙ ⴰⵎⵉⴹⴰⵏ + Remove the account %1$s from the application? + ⴰⵣⵏ ⵉⵎⴰⵢⵍ + Tap on the path to change it + Failed! + Scheduled toots + Information below may reflect the user\'s profile incompletely. + ⵔⵏⵓ ⵉⵎⵓⵊⵉ + The app did not collect custom emojis for the moment. + Push notifications + ⵉⵙ ⵏⵉⵜ ⵜⵅⵙⴼ ⴰⴷ ⵜⴼⴼⵖⴷ? + ⵉⵙ ⵏⵉⵜ ⵜⵅⵙⴼ ⴰⴷ ⵜⴼⴼⵖⴷ @%1$s@%2$s? + + No toot to display + No stories to display + Stories + Boosted by %1$s + Add this toot to your favourites? + Remove this toot from your favourites? + Boost this toot? + Unboost this toot? + Pin this toot? + Unpin this toot? + ⵙⵙⵓⵙⵎ + ⴳⴷⵍ + ⵎⵍ + ⴽⴽⵙ + ⵙⵙⵏⵖⵍ + ⴱⴹⵓ + Mention + Timed mute + Delete & re-draft + + Mute this account? + Block this account? + Report this toot? + Block this domain? + Unmute this account? + Unblock this account? + + + Notify + ⵙⵙⵓⵙⵎ + + + Delete this toot? + Delete & re-draft this toot? + + Bookmarks + Add to bookmarks + Remove bookmark + No bookmarks to display + Status has been added to bookmarks! + Status was removed from bookmarks! + + %d ⵙⵏ + %d ⵙⴷ + %d ⵙⵔⴳ + %d ⴰⵙⵙ + + %d ⵜⵙⵉⵏⵜ + %d ⵜⵉⵙⵉⵏⴰ + + + %d ⵜⵓⵙⴷⵉⴷⵜ + %d ⵜⵓⵙⴷⵉⴷⵉⵏ + + + %d ⵜⵙⵔⴰⴳⵜ + %d ⵜⵙⵔⴰⴳⵉⵏ + + + %d ⵡⴰⵙⵙ + %d ⵡⵓⵙⵙⴰⵏ + + + Warning + ⵎ\'ⴰⵢⴷ ⵉⵍⵍⴰⵏ ⴳ ⵢⵉⵖⴼ ⵏⵏⴽ? + TOOT! + QUEET! + cw + Write a toot + Reply to a toot + Write a queet + Reply to a queet + Select a media + An error occurred while selecting the media! + Remove this media? + Your toot is empty! + Visibility of the toot + Visibility of the toots by default: + The toot has been sent! + You are replying to this toot: + Sensitive content? + + Post to public timelines + Do not post to public timelines + Post to followers only + Post to mentioned users only + + No drafts! + Choose a toot + ⴷⵖⵔ ⵢⴰⵏ ⵓⵎⵉⴹⴰⵏ + ⵙⵜⵢ ⴽⵔⴰ ⵏ ⵉⵎⵉⴹⴰⵏⵏ + Delete draft? + Tap on the button to display the original toot + Describe for the visually impaired + + No description available! + + Release %1$s + ⴰⵎⵙⵖⵉⵡⵙ: + ⵜⵓⵔⴰⴳⵜ: + GNU GPL V3 + Source code: + Translation of toots: + Search instances: + Icon designer: + + ⴰⵎⵙⴰⵡⴰⵍ + + No account to display + No follow request + Toots \n %1$s + Following \n %1$s + ⵉⵎⴹⴼⴰⵕⵏ \n %1$s + Pinned \n %d + ⵙⵙⵓⵔⴳ + ⴰⴳⵢ + + No scheduled toots to display! + Write a toot and then choose Schedule from the top menu. + Delete scheduled toot? + Media: %d + The toot has been scheduled! + The scheduled date must be greater than the current hour! + Battery saver is enabled! It might not work as expected. + + The time for muting should be greater than one minute. + %1$s has been muted until %2$s.\n You can unmute this account from their profile page. + %1$s is muted until %2$s.\n Tap here to unmute the account. + + No notification to display + mentioned you + wrote a new message + boosted your status + favourited your status + followed you + asked to follow you + + and another notification + and %d other notifications + + + %d like + %d likes + + Delete a notification? + Delete all notifications? + The notification has been deleted! + All notifications have been deleted! + + Following + ⵉⵏⴹⴼⴰⵕⵏ + Pinned + + Unable to get client id! + Unable to connect to instance domain! + No Internet connection! + The account was blocked! + The account is no longer blocked! + The account was muted! + The account is no longer muted! + The account was followed! + The account is no longer followed! + The toot was boosted! + The toot is no longer boosted! + The toot was added to your favourites! + The toot was removed from your favourites! + The toot was reported! + The toot was deleted! + The toot was pinned! + The toot was unpinned! + Oops ! An error occurred! + An error occurred! The instance did not return an authorisation code! + The instance domain does not seem to be valid! + An error occurred while switching between accounts! + An error occurred while searching! + The profile data have been saved! + No action can be taken + The media has been saved! + An error occurred while translating! + Translations are disabled in settings + Draft saved! + Are you sure this instance allows this number of characters? Usually, this value is close to 500 characters. + Visibility of the toots has been changed for the account %1$s + + Number of toots per load + ⴰⴱⴷⴰ + ⴰⵡⵉⴼⵉ + ⵙⵙⵓⵜⵔ + Load the media + Load the pictures + ⵙⵎⴰⵍ ⵓⴳⴳⴰⵔ… + ⵙⵎⴰⵍ ⴷⵔⵓⵙ… + Sensitive content + Disable GIF avatars + ⴰⴱⵔⵉⴷ: + Save drafts automatically + Add URL of media in toots + Notify when someone follows you + Notify when someone boosts your status + Notify when someone favourites your status + Notify when someone mentions you + Notify when a poll ended + Notify for new posts + Show confirmation dialog before boosting + Show confirmation dialog before adding to favourites + Notify in WIFI only + Notify? + Silent Notifications + NSFW view timeout (seconds, 0 means off) + Media Description timeout (seconds, 0 means off) + ⵙⵏⴼⵍ ⵉⴼⵔⵙ + Custom sharing + Your custom sharing URL… + Bio… + ⵔⴳⵍ ⴰⵎⵉⴹⴰⵏ + ⵃⴹⵓ ⵉⵙⵏⴼⵍⵏ + Choose a header picture + Fit preview images + Automatically split toots in replies when chars are over: + You have reached the 160 characters allowed! + You have reached the 30 characters allowed! + ⴳⵔ + + The time must be greater than %1$s + The time must be lower than %1$s + Start time + End time + Use the built-in browser + Custom tabs + Enable Javascript + Automatically expand cw + Allow third-party cookies + Your API key, you can leave blank for Yandex + + ⴰⴷⵖⵎⵓⵎ + ⴰⵏⴰⴼⴰⵡ + ⴰⴱⵔⴽⴰⵏ + + Set LED colour: + + Blue + Cyan + Magenta + Green + ⴰⵣⴳⴳⵯⴰⵖ + ⴰⵡⵔⴰⵖ + ⴰⵎⵍⵍⴰⵍ + + ⴹⴼⵕ + Unblock + ⵙⵙⵓⵙⵎ + ⴽⴽⵙ ⴰⵙⵓⵙⵎ + Request sent + Follows you + ⵔⵣⵓ + First letter in capital for replies + Resize pictures + Resize videos + + Push notifications + Please, confirm push notifications that you want to receive. + You can enable or disable these notifications later in settings (Notifications tab). + + + Clear cache + There are %1$s of data in cache.\n\nWould you like to delete them? + ⵎⴱ + Cache was cleared! %1$s were released + + ⴰⵣⵡⵍ + ⴰⵣⵡⵍ… + Description + Keywords + Keywords… + + Synchronize + ⵙⵜⵉ + ⵉⵎⴰⵙⵙⵏ ⵏⵏⴽ + Your notifications + ⴰⴳⴷⵓⴷⴰⵏ + Unlisted + ⵓⵙⵍⵉⴳ + Direct + Some keywords… + Show media + Show pinned + No matching result found! + Backup toots for %1$s + %1$s new toots have been imported + %1$s new notifications have been imported + + Dates descending + Dates ascending + + + ⵓⵀⵓ + ⵖⴰⵙ + ⵙ ⵙⵉⵏ + + No toots were found in database. Please, use the synchronize button from the menu to retrieve them. + + Recorded data + Only basic information from accounts are stored on the device. + These data are strictly confidential and can only be used by the application. + Deleting the application immediately removes these data.\n + ⚠ Login and passwords are never stored. They are only used during a secure authentication (SSL) with an instance. + + Permissions: + - ACCESS_NETWORK_STATE: Used to detect if the device is connected to a WIFI network.\n + - INTERNET: Used for queries to an instance.\n + - WRITE_EXTERNAL_STORAGE: Used to store media or to move the app on a SD card.\n + - READ_EXTERNAL_STORAGE: Used to add media to toots.\n + - BOOT_COMPLETED: Used to start the notification service.\n + - WAKE_LOCK: Used during the notification service. + + API permissions: + - Read: Read data.\n + - Write: Post statuses and upload media for statuses.\n + - Follow: Follow, unfollow, block, unblock.\n\n + ⚠ These actions are carried out only when user requests them. + + Tracking and Libraries + The application does not use tracking tools (audience measurement, error reporting, etc.) and does not contain any advertising.\n\n + The use of libraries is minimized: \n + - Glide: To manage media\n + - Android-Job: To manage services\n + - PhotoView: To manage images\n + + Translation of toots + The application offers the ability to translate toots using the locale of the device and the Yandex API.\n + Yandex has its proper privacy-policy which can be found here: https://yandex.ru/legal/confidential/?lang=en + + Thank you to: + + Filter out by regular expressions + ⵔⵣⵓ + ⴽⴽⵙ + Fetch more toots… + + Lists + Are you sure you want to permanently delete this list? + There is nothing in this list yet. When members of this list post new statuses, they will appear here. + Add to list + Add list + Delete list + Edit list + New list title + The account was added to the list! + You don\'t have any lists yet! + + %1$s has moved to %2$s + Authentication does not work? + Here are some checks that might help:\n\n + - Check there is no spelling mistakes in the instance name\n\n + - Check that your instance is not down\n\n + - If you use the two-factor authentication (2FA), please use the link at the bottom (once the instance name is filled)\n\n + - You can also use this link without using the 2FA\n\n + - If it still does not work, please raise an issue on FramaGit at https://framagit.org/tom79/fedilab/issues + + Media has been loaded. Tap here to display it. + This action can be quite long. You will be notified when it will be finished. + Still running, please wait… + Export statuses + Export statuses for %1$s + %1$s toots out of %2$s have been exported. + Something went wrong when exporting data for %1$s + Something went wrong when exporting data! + Something went wrong when importing data! + + ⴰⵒⵕⵓⴽⵙⵉ + Enable proxy? + Host + Port + Login + Password + Add toot details when sharing + Support the app on Liberapay + There is an error in the regular expression! + No timelines was found on this instance! + Delete this instance? + ⵙⵙⵓⵖⵍ ⵙ + Follow instance + You already follow this instance! + The instance is followed! + Partnerships + Information + Hide boosts from %s + Feature on profile + Show boosts from %s + Don\'t feature on profile + The account is now featured on profile + The account is no longer featured on profile + Boosts are now shown! + Boosts are now hidden! + Direct message + ⴰⵙⵜⴰⵢ + No filters to display. You can create one by tapping on the \"+\" button. + Keyword or phrase + Home timeline + Public timelines + Notifications + Conversations + Will be matched regardless of casing in text or content warning of a toot + Drop instead of hide + Filtered toots will disappear irreversibly, even if filter is later removed + When the keyword or phrase is alphanumeric only, it will only be applied if it matches the whole word + Whole word + Filter contexts + One or multiple contexts where the filter should apply + Expire after + ⴽⴽⵙ ⴰⵙⵜⴰⵢ? + ⵙⴷⵖⵉ ⴰⵙⵜⴰⵢ + ⵔⵏⵓ ⴰⵙⵜⴰⵢ + Whom to follow + There is no accounts listed for the moment! + ⴹⴼⵕ + Select all + Unselect all + %s is followed! + Creating the list %s + Adding accounts to the list + Accounts were added to the list + Adding accounts to the list + You have not created a list yet. Tap on the \"+\" button to add a new one. + Who to follow + Trunk API + Account(s) can\'t be followed + Fetching remote account + Automatically expand hidden media + New follow + New Boost + New Favourite + New Mention + Poll Ended + New Toot + Toots Backup + New posts + Media Download + Change notification sound + Select Tone + Enable time slot + How To Videos + Fetching remote thread! + No blocked domains! + Unblock domain + Are you sure to unblock %s? + Are you sure to block %s?\n\nYou will not see any content from that domain in any public timeline or in your notifications. Your followers from that domain will be removed. + Blocked domains + Block domain + The domain is blocked + The domain is no longer blocked! + Fetching remote status + ⵅⴼⴰⵡⵍ + Peertube instance + Be the first to leave a comment on this video with the top right button! + %s ⵏ ⵜⴰⵏⵏⴰⵢⵉⵏ + Duration: %s + Add an instance + Comments are not enabled on this video! + Pick up a resolution + Peertube favourites + The video has been added to bookmarks! + The video has been removed from bookmarks! + There is no Peertube videos in your favourites! + ⴰⵙⴰⵔⵓ + ⴰⴼⵉⴷⵢⵓ + ⵉⵙⵓⵔⴰ + Use Emoji One + Information + Display previews in all toots + New UX/UI designer + Display video previews + The account id has been copied in the clipboard! + Change the language + Default language + Truncate long toots + Truncate toots over \'x\' lines. Zero means disabled. + Display more + Display less + Manage tags + The tag already exists! + The tag has been stored! + The tag has been changed! + The tag has been removed! + Schedule boost + The boost is scheduled! + No scheduled boost to display! + Schedule boost.]]> + Art timeline + Open menu + ⴷⵡⵍ + Logo of the application + Profile picture + Profile banner + Contact admin of the instance + ⵔⵏⵓ ⴰⵎⴰⵢⵏⵓ + MastoHost logo + Emoji picker + Refresh + Expand the conversation + ⴽⴽⵙ ⴰⵎⵉⴹⴰⵏ + Remove the blocked domain + Custom emoji picker + ⵖⵔ ⴰⴼⵉⴷⵢⵓ + New toot + Image of the card + Hide media + Favicon + Add description for media (for the visually impaired) + + Never + 30 minutes + 1 ⵜⵙⵔⴰⴳⵜ + 6 ⵜⵙⵔⴰⴳⵉⵏ + 12 ⵜⵙⵔⴰⴳⵉⵏ + 1 ⵡⴰⵙⵙ + 1 ⵉⵎⴰⵍⴰⵙⵙ + + In this field, you need to write your instance domain.\nFor example, if you created your account on https://mastodon.social\nJust write mastodon.social\nYou can start writing first letters and names will be suggested. + + More information + + ⵜⵓⵜⵍⴰⵢⵉⵏ + Media only + Show NSFW + Crowdin translations + Crowdin manager + Translation of the application + About Crowdin + ⴰⴱⵓⵜ + Pixelfed instance + Mastodon instance + Any of these + All of these + None of these + Any of these words (space-separated) + All these words (space-separated) + Add some words to filter (space-separated) + Change column name + Misskey instance + No app supporting this link is installed on your device. + Subscriptions + Overview + Trending + Recently added + Local + Upload + ⵔⴰⵔ + ⴽⴽⵙ ⴰⵅⴼⴰⵡⴰⵍ + Are you sure to delete this comment? + Full screen video + Mode for videos + Select the file to upload + ⵉⴼⵉⴷⵢⵓⵜⵏ ⵉⵏⵡ + ⴰⵣⵡⵍ + ⵜⵓⵔⴰⴳⵜ + Category + ⵜⵓⵜⵍⴰⵢⵜ + This video contains mature or explicit content + Enable video comments + ⵙⴷⵖⵉ ⴰⴼⵉⴷⵢⵓ + Description + The video has been updated! + Upload cancelled! + The video has been uploaded! + Uploading, please wait… + Tap here to edit the video data. + ⴽⴽⵙ ⴰⴼⵉⴷⵢⵓ + Are you sure to delete this video? + Display NSFW videos + No videos to display! + Leave a comment + ⴱⴹⵓ + Choose a schedule mode + From device + From server + Toots (Server) + Toots (Device) + Modify + Display new toots above the \"Fetch more\" button + Timelines + Interface + ⴰⵙⵙⴰⵖⵏ + %1$s commented your video %2$s]]> + %1$s is following your channel %2$s]]> + %1$s is following your account]]> + %1$s has been published]]> + %1$s succeeded]]> + %1$s failed]]> + %1$s published a new video: %2$s]]> + %1$s has been blacklisted]]> + %1$s has been unblacklisted]]> + Export data + Import Data + Select the file to import + An error occurred when selecting the backup file! + Add a public comment + Send comment + There is no Internet connection. Your message has been stored in drafts. + Plain text + HTML + Markdown + Logout account + ⵎⴰⵕⵕⴰ + Support the app + Open Collective enables groups to quickly set up a collective, raise funds and manage them transparently. + ⵙⵙⵏⵖⵍ ⴰⵙⵖⵏ + ⵣⴷⵢ + Normal + Compact + Console + Set display mode + Patch the Security Provider + Update tracking domains + The tracking data base has been updated! + http calls blocked by the application + List of blocked calls + Submit + The data base has been exported! + Featured hashtags + Filter timeline with tags + No tags + Hide the \"delete\" button in the notification tab + Attach an image when sharing a URL + + ⵉⴷⵣ + ⵉⴷⵣⵏ + ⵔⵏⵓ ⵉⴷⵣ + Choice 1 + Choice 2 + Choice %d + You need two choices at least for the poll! + Done + end at %s + Refresh poll + Vote + A poll you have voted in has ended + A poll you tooted has ended + Customize + Categories + Time slot + Advanced + Display \'new\' badge on unread toots + Peertube + Move timeline + Hide timeline + Reorder timelines + List permanently deleted + Followed instance removed + Pinned tag removed + Undo + You need to keep two visible tabs! + Reorder timelines + Main timelines can only be hidden! + BBCode + Always mark media as sensitive + GNU instance + Cached status + Forward tags in replies + Long press to store media + Blur sensitive media + Display timelines in a list + Display timelines + Mark bot accounts in toots + Manage tags + Remember the position in Home timeline + ⴰⵎⵣⵔⵓⵢ + Playlists + Display name + You don\'t have any playlists. Tap on the \"+\" icon to add a new playlist + You must provide a display name! + The channel is required when the playlist is public. + Create a playlist + There is nothing in this playlist yet. + redo + Gallery + ⵉⵎⵓⵊⵉ + Sticker + Eraser + ⴰⴹⵕⵉⵚ + ⴰⵙⵜⴰⵢ + Brush + Are you sure you want to exit without saving the image? + Discard + Saving… + Image Saved Successfully! + Failed to save Image + Opacity + Enable photo editor + Add a poll item + Remove last poll item + Mute conversation + Unmute conversation + The conversation is no longer muted! + The conversation is muted + Open application features + Timed mute + Mention the account + Refresh cache + Mention the status + News + ⴰⵎⴰⵜⴰⵢ + ⴰⵏⵎⵏⴰⴹ + Art + Journalism + Activism + ⵓⵔⴰⵔⵏ + ⵜⴰⵜⵉⴽⵏⵓⵍⵓⵊⵉⵜ + Adult content + Furry + ⵓⵜⵛⵉ + Logo of the instance + Something went wrong when checking available instances! + Join Mastodon + Choose an instance by picking up a category, then tap on a check button. + Choose an instance by tapping on a check button. + %1$s users + Confirm password + I agree to %1$s and %2$s + server rules + terms of service + Sign up + This instance works with invitations. Your account will need to be manually approved by an administrator before being usable. + Please, fill all the fields! + Passwords don\'t match! + The email doesn\'t seem to be valid! + Your username will be unique on %1$s + You will be sent a confirmation e-mail + Use at least 8 characters + Password should contain at least 8 characters + Username should only contain letters, numbers and underscores + Account created! + Your account has been created!\n\n + Think to validate your email within the 48 next hours.\n\n + You can now connect your account by writing %1$s in the first field and tap on Connect.\n\n + Important: If your instance required validation, you will receive an email once it is validated! + + Save the message in drafts? + ⵜⴰⵎⵙⵙⵓⴳⵓⵔⵜ + Reports + No reports to display! + Reconnect the account + The application failed to access the admin features. You might need to reconnect the account for having the correct scope. + Unresolved + Remote + Active + Pending + Disabled + Silenced + Suspended + Permissions + Email status + Login status + ⵉⵍⴽⵎ + Most recent IP + ⵙⴽⴰⵣ + Disable + Silence + Notify the user per e-mail + Custom warning + User + Moderator + ⴰⵎⵙⵙⵓⴳⵓⵔ + Confirmed + Not confirmed + Reported statuses + ⴰⵎⵉⴹⴰⵏ + Undo silence + Undo disable + Suspend + Undo suspend + The account is silenced! + The account is no longer silenced! + The account is suspended! + The account is no longer suspended! + The account is disabled! + The account is no longer disabled! + The account has been warned! + Display the admin menu + Display the admin feature in statuses + Allow + The account is approved! + The account is rejected! + Assign to me + Unassign + Mark as resolved + Mark as unresolved + Empty content! + Display Fedilab features button + The application needs to access audio recording + Voice message + Enable quick reply + The account you are replying might not see your message! + If disabled, the app will always load last statuses + If disabled, sensitive media will be hidden with a button + Store media in full size with a long press on previews + Add an ellipse button at the top right for listing all tags/instances/lists + During the time slot, the app will send notifications. You can reverse (ie: silent) this time slot with the right spinner. + Display a Fedilab button below profile picture. It is a shortcut for accessing in-app features. + Allow to reply directly in timelines below statuses + Previews will not be cropped in timelines + Allow to play embedded videos directly in timelines + Allow to reverse the way to read statuses that are displayed once tapping the fetch more button + This option allows to support recent cipher suites. It is useful for older Android devices or if you cannot connect to your instance. + Exclusively for Peertube videos. Switch this mode if you cannot play them. + These tags will allow to filter statuses from profiles. You will have to use the context menu for seeing them. + Automatically insert a line break after the mention to capitalize the first letter + Allow content creators to share statuses to their RSS feeds + Compose + Maximum retry times when uploading media + Create a new Folder here + Enter the folder name + Please enter a valid folder name + This folder already exists.\n Please provide another name for the folder + Select + Default Directory + Folder + Create folder + Display a toast message after an action has been completed (boost, fav, etc.)? + Muted instances have been exported! + Add an instance + Export instances + Import instances + Crash reports + Enable crash reports + If enabled, a crash report will be created locally and then you will be able to share it. + Fedilab has stopped :( + You can send me by email the crash report. It will help to fix it :)\n\nYou can add additional content. Thank you! + Use the wysiwyg + When enabled, you will be able to format your text easily with tools. + Statistics + Total statuses + Number of boosts + Number of favourites + Number of mentions + Number of follows + Number of polls + Number of replies + Number of statuses + Statuses + Visibility + Number with media + Number with sensitive media + Number with CW + First status date + Last status date + First notification date + Last notification date + Frequency + %s statuses per day + %s notifications per day + Date range + ⵜⵉⵔⵓⴱⴱⴰ + No groups! + Disable custom animated emojis + Charts + Display charts + The application collects your local data, please wait… + Backup + Auto backup statuses + This option is per account. It will launch a service that will automatically store your statuses locally in the database. That allows to get statistics and charts + Auto backup notifications + This option is per account. It will launch a service that will automatically store your notifications locally in the database. That allows to get statistics and charts + Report account + Send an invitation + Your instance does not allow to register a new account! + + %d vote + %d votes + + + %d voter + %d voters + + + Single choice + Multiple choices + + + 5 minutes + 30 minutes + 1 ⵜⵙⵔⴰⴳⵜ + 6 ⵜⵙⵔⴰⴳⵉⵏ + 1 ⵡⴰⵙⵙ + 3 ⵡⵓⵙⵙⴰⵏ + 7 ⵡⵓⵙⵙⴰⵏ + + + Webview + Direct stream + + For joining my instance \"%1$s\", you can download Fedilab:\n\nF-Droid: %2$s\nGoogle: %3$s\n\nThen open the link below with Fedilab and create your account :)\n\n%4$s + + Your poll can\'t have duplicated options! + For all accounts + Database cache + Clear your home timeline cache + Clear your cached statuses + Clear your bookmarks + Files in cache + Total notifications + Hide menu items + Fedilab is running live notifications + For %1$s accounts with %2$s events + Live notifications for %1$s + Live notifications will be enabled for this account. + Clear cache when leaving + The cache (media, cached messages, data from the built-in browser) will be automatically cleared when leaving the application. + Do you want to unfollow this account? + Show confirmation dialog before unfollowing + Replace Youtube with Invidio.us + Invidious is an alternative front-end to YouTube + Enter your custom host or leave blank for using invidious.snopyta.org + Replace Twitter with Nitter + Nitter is an open source alternative Twitter front-end focused on privacy. + Enter your custom host or leave blank for using nitter.net + Replace Instagram with Bibliogram + Bibliogram is an open source alternative Instagram front-end focused on privacy. + Enter your custom host or leave blank for using bibliogram.art + Replace Reddit with Libreddit + Libreddit is an open source alternative Reddit front-end focused on privacy. + Enter your custom host or leave blank for using libredd.it + Replace Medium links + Replace medium.com links with an open source alternative front-end focused on privacy. + Default: scribe.rip + Replace Wikipedia links + Replace Wikipedia link with an open source alternative front-end focused on privacy. + Default: wikiless.org + Hide Fedilab notification bar + For hiding the remaining notification in the status bar, tap on the eye icon button then uncheck: \"Display in status bar\" + Use a push notifications system for getting notifications in real time. + No live notifications + Live notifications + Notifications will be fetched every 15 minutes. + Add notes + Notes for the account + Allow to compress large photos into smaller sized photos with very less or negligible loss in quality of the image. + Allow compressing videos while maintaining their quality. + The app is compressing the media, it can be quite long… + Change app icon + Tap to change the app icon + Post + Visibility of the post + Tap here to add photos + Accepted Formats: jpeg, png, gif \n\nMax File Size: 15 MB \n\nAlbums can contain up to 4 photos or videos + Upload media + Add an optional caption + The app received a very long error message from the API %1$s + Message preview + Add mentions in each message + Fetching conversation + Order by + Title for the video + Join Peertube + I am at least 16 years old and agree to the %1$s of this instance + Links + Change the color of links (URLs, mentions, tags, etc.) in messages + Reblogs header + Change the color of display name at the top of messages + Change the color of the user name at the top of messages + Change the color of the header for reblogs + Posts + Background color of posts in timelines + Reset colors + Tap here to reset all your custom colors + Reset + ⵉⴽⵓⵏ + Color of bottom icons in timelines + Pin this tag + Logo of the instance + Edit profile + Make an action + Translation + Image preview + Text color + Change the text color in pots + Apply changes + You need to restart the application to apply changes + Restart + Use a custom theme + Allow to override colors of the selected theme above + Theming + Store before + The theme was exported + The theme has been successfully exported in CSV + Apply the primary color to the status bar + Status bar color + Restore a default theme + Import a theme + Tap here to import a theme from a previous export + Export the theme + Tap here to export the current theme + An error occurred when selecting the theme file + Theme Picker + Select a pre-installed theme + Themes + Apply the primary color to the navigation bar + Navigation bar color + The underlying color of the app’s content. + Background color + Accents select parts of the UI. + Accent color + Displayed most frequently across your app. + Primary color + Export bookmarks to the instance + Import bookmarks from the instance + User count + Status count + Instance count + Blocked + End in %s + What\'s new in %s + You can follow my account for updates + This instance is not available on https://instances.social + Display full link + Share link + The URL has been copied to the clipboard + Open with another app + Check redirect + This URL does not redirect + %1$s \n\nredirects to\n\n %2$s + Change the user agent + Set a custom user agent or leave blank + Allows to customize the user agent used for api calls or with the built-in browser. + Remove UTM parameters + The app will automatically remove UTM parameters from URLs before visiting a link. + Trends + Trending now + %d people talking + Twitter accounts (via Nitter) + Twitter usernames space separated + Identity proofs + Verified identity + Verified by %1$s (%2$s) + Delete the notification + Display more options + It is a Pixelfed story + Upload a media, it will be automatically added to your Pixelfed story. + Media successfully added to your story! + Action disabled + Unfollow + Something went wrong, please check your download directory in settings. + Announcements + No announcements! + Add a reaction + Use your favourite browser inside the app. Uncheck this feature to open links externally. + Video cache in MB, zero means no cache. + Watermarks + Automatically add a watermark at the bottom of pictures. The text can be customized for each account. + No distributors found! + You need a distributor for receiving push notifications.\nYou will find more details at %1$s.\n\nYou can also disable push notifications in settings for ignoring that message. + Select a distributor + diff --git a/app/src/main/res/values-bn/strings.xml b/app/src/main/res/values-bn/strings.xml new file mode 100644 index 000000000..bcb004429 --- /dev/null +++ b/app/src/main/res/values-bn/strings.xml @@ -0,0 +1,1142 @@ + + + মেনু খুলুন + মেনু বন্ধ করুন + সম্পর্কে + ইনস্ট্যান্স এর সম্পর্কে + গোপনীয়তা + ক্যাশে + লগ আউট + লগইন + + বন্ধ + হ্যাঁ + না + বাতিল + ডাউনলোড + ডাউনলোড করা হচ্ছে %1$s + মিডিয়া সংরক্ষিত + ফাইল: %1$s + পাসওয়ার্ড + ই-মেইল + অ্যাকাউন্টগুলি + টুটগুলি + ট্যাগগুলি + সংরক্ষণ + পুনঃস্থাপন + কোনও ফলাফল পাওয়া যায় নি! + ইনস্ট্যান্স + ইনস্ট্যান্স: mastodon.social + %1$s এখন কাজ করছে + একটি অ্যাকাউন্ট যোগ করুন + টুটের সামগ্রীটি ক্লিপবোর্ডে অনুলিপি করা হয়েছে + টুটের URL টি ক্লিপবোর্ডে অনুলিপি করা হয়েছে + পরিবর্তন + একটি ছবি নির্বাচন করুন… + পরিষ্কার + ক্য়ামেরা + সব মুছে দিন + টুট টি অনুবাদ করুন। + সময়সূচি + পাঠ্য এবং আইকনের আকার + বর্তমান পাঠ্য আকার পরিবর্তন করুন: + বর্তমান আইকন আকার পরিবর্তন করুন: + পরবর্তী + পূর্ববর্তী + দিয়ে খুলুন + যাচাই + মিডিয়া + সাথে শেয়ার + ফেডিলাবের মাধ্যমে ভাগ করা হয়েছে + উত্তরগুলি + ব্যবহারকারীর নাম + ড্রাফটগুলি + প্রিয়াগুলি + নতুন অনুসরণকারী + উল্লেখগুলি + সমর্থনগুলি + সমর্থনগুলি দেখান + উত্তরগুলি দেখান + ব্রাউজারে খুলুন + অনুবাদ + অনুগ্রহ করে, এই ক্রিয়াটি করার আগে কয়েক সেকেন্ড অপেক্ষা করুন। + + গৃহ + স্থানীয় সময়রেখা + সংযুক্ত সময়রেখা + বিকল্পগুলি + প্রিয়াগুলি + যোগাযোগ + নিঃশব্দ ব্যবহারকারী + অবরুদ্ধ ব্যবহারকারী + বিজ্ঞপ্তিগুলি + অনুসরণ অনুরোধগুলি + বিন্যাস + একটি অ্যাকাউন্ট মুছেন + আপনি কি এই %1$s একাউন্ট টি এপ্লিকেশন থেকে মুছে ফেলতে চান? + একটি ইমেইল পাঠান + এটি পরিবর্তন করার জন্য পথে ক্লিক করুন + ব্যর্থ! + নির্ধারিত টুটগুলি + নীচের তথ্যগুলি ব্যবহারকারীর প্রোফাইলকে অসম্পূর্ণভাবে প্রতিবিম্বিত করতে পারে। + ইমোজি ঢোকান + অ্যাপটি মুহুর্তের জন্য কাস্টম ইমোজিগুলি সংগ্রহ করে নি। + Push notifications + Are you sure you want to logout? + Are you sure you want to logout @%1$s@%2$s? + + প্রদর্শনের জন্য কোন টুট নেই + No stories to display + Stories + %1$s দ্বারা সমর্থন + এই টুটটি আপনার পছন্দসইয়ের সাথে যুক্ত করবেন? + আপনার পছন্দসই থেকে এই টুট সরাবেন? + এই টুট টি সমর্থন করবেন? + এই টুট টি অসমর্থন করবেন? + এই টুট টি পিন করবেন? + এই টুট টি আনপিন করবেন? + নীরব + ব্লক + প্রতিবেদন + সরান + অনুলিপি + বিভাজিত + উল্লেখ + সময়যুক্ত নিঃশব্দ + মুছে & পুনরায় খসড়া + + এই অ্যাকাউন্ট নিঃশব্দ করবেন? + এই অ্যাকাউন্ট ব্লক করবেন? + এই টুট টি প্রতিবেদন করবেন? + এই ডোমেইন ব্লক করবেন? + এই একাউন্ট টি কে নিঃশব্দ তালিকা থেকে সরাবেন? + Unblock this account? + + + বিজ্ঞপ্তি + নিঃশব্দ + + + এই টুট টি মুছে ফেলবেন? + এই টুট টি মুছে & পুনঃসংশ্লিষ্ট করবেন? + + বুকমার্কগুলি + বুকমার্ক তালিকা এ যোগ করুন + বুকমার্ক মুছে ফেলুন + প্রদর্শনের জন্য কোনও বুকমার্ক নেই + বুকমার্কগুলিতে স্ট্যাটাস যুক্ত হয়েছে! + বুকমার্কগুলি থেকে স্থিতি সরানো হয়েছিল! + + %d সেকেন্ড + %d মিনিট + %d ঘন্টা + %d দিন + + %d second + %d সেকেন্ড + + + %d minute + %d মিনিট + + + %d hour + %d ঘণ্টা + + + %d day + %d দিন + + + সতর্কতা + আপনার মনে কি আছে? + টুট! + ক্যুইট! + সঃবঃ + একটি টুট লিখুন + একটি টুট এ উত্তর দিন + একটি ক্যুইট লিখুন + একটি ক্যুইট এ উত্তর দিন + একটি মিডিয়া নির্বাচন করুন + মিডিয়া নির্বাচন করার সময় একটি ত্রুটি ঘটেছে! + এই মিডিয়া মুছবেন? + আপনার টুট ফাঁকা! + টুটের দৃশ্যমানতা + ডিফল্টরূপে টুটগুলির দৃশ্যমানতা: + টুট পাঠানো হয়েছে! + আপনি এই টুটের উত্তর দিচ্ছেন: + সংবেদনশীল কন্টেন্ট? + + জনসাধারণের টাইমলাইনে পোস্ট করুন + জনসাধারণের টাইমলাইনে পোস্ট করবেন না + শুধুমাত্র অনুসরণকারীদের পোস্ট করুন + শুধুমাত্র উল্লিখিত ব্যবহারকারীদের জন্য পোস্ট করুন + + কোন খসড়া নেই! + একটি টুট চয়ন করুন + একটি অ্যাকাউন্ট চয়ন করুন + কিছু অ্যাকাউন্টগুলি নির্বাচন করুন + খসড়া সরান? + আসল টুটটি প্রদর্শন করতে বোতামে আলতো চাপুন + দৃষ্টি প্রতিবন্ধীদের জন্য বর্ণনা করুন + + কোন বর্ণনা নাই! + + রিলিজ %1$s + বিকাশকারী: + লাইসেন্স: + জিএনইউ জিপিএল ভার্সন ৩ + Source code: + Translation of toots: + Search instances: + Icon designer: + + Conversation + + No account to display + No follow request + Toots \n %1$s + Following \n %1$s + Followers \n %1$s + Pinned \n %d + Authorize + Reject + + No scheduled toots to display! + Write a toot and then choose Schedule from the top menu. + Delete scheduled toot? + Media: %d + The toot has been scheduled! + The scheduled date must be greater than the current hour! + Battery saver is enabled! It might not work as expected. + + The time for muting should be greater than one minute. + %1$s has been muted until %2$s.\n You can unmute this account from their profile page. + %1$s is muted until %2$s.\n Tap here to unmute the account. + + No notification to display + mentioned you + wrote a new message + boosted your status + favourited your status + followed you + asked to follow you + + and another notification + and %d other notifications + + + %d like + %d likes + + Delete a notification? + Delete all notifications? + The notification has been deleted! + All notifications have been deleted! + + Following + Followers + Pinned + + Unable to get client id! + Unable to connect to instance domain! + No Internet connection! + The account was blocked! + The account is no longer blocked! + The account was muted! + The account is no longer muted! + The account was followed! + The account is no longer followed! + The toot was boosted! + The toot is no longer boosted! + The toot was added to your favourites! + The toot was removed from your favourites! + টুটটির প্রতিবেদন করা হয়েছে! + টুটটি মুছে ফেলা হয়েছে! + টুটটি পিন করা হয়েছে! + টুটটি আনপিন করা হয়েছে! + উফফফফ! একটি ত্রুটি ঘটেছে! + একটি ত্রুটি ঘটেছে! ইনস্ট্যান্স একটি অনুমোদন কোড ফেরায় নি! + ইনস্ট্যান্স এর ডোমেনটি বৈধ বলে মনে হচ্ছে না! + অ্যাকাউন্টগুলির মধ্যে স্যুইচ করার সময় একটি ত্রুটি ঘটেছে! + অনুসন্ধানের সময় একটি ত্রুটি ঘটেছে! + প্রোফাইল তথ্য সংরক্ষণ করা হয়েছে! + কোনও পদক্ষেপ নেওয়া যাবে না + মিডিয়া রক্ষা পেয়েছে! + অনুবাদ করার সময় একটি ত্রুটি ঘটেছে! + অনুবাদসমূহ সেটিংসে অক্ষম রয়েছে + খসড়া সংরক্ষিত করা হয়েছে! + Are you sure this instance allows this number of characters? Usually, this value is close to 500 characters. + Visibility of the toots has been changed for the account %1$s + + Number of toots per load + Always + WIFI + Ask + Load the media + Load the pictures + Show more… + Show less… + Sensitive content + Disable GIF avatars + Path: + Save drafts automatically + Add URL of media in toots + Notify when someone follows you + Notify when someone boosts your status + Notify when someone favourites your status + Notify when someone mentions you + Notify when a poll ended + Notify for new posts + Show confirmation dialog before boosting + Show confirmation dialog before adding to favourites + Notify in WIFI only + Notify? + Silent Notifications + NSFW view timeout (seconds, 0 means off) + Media Description timeout (seconds, 0 means off) + Edit profile + Custom sharing + Your custom sharing URL… + Bio… + Lock account + Save changes + Choose a header picture + Fit preview images + Automatically split toots in replies when chars are over: + You have reached the 160 characters allowed! + You have reached the 30 characters allowed! + Between + and + The time must be greater than %1$s + The time must be lower than %1$s + Start time + End time + Use the built-in browser + Custom tabs + Enable Javascript + Automatically expand cw + Allow third-party cookies + Your API key, you can leave blank for Yandex + + Dark + Light + Black + + Set LED colour: + + Blue + Cyan + Magenta + Green + Red + Yellow + White + + Follow + Unblock + Mute + Unmute + Request sent + Follows you + Search + First letter in capital for replies + Resize pictures + Resize videos + + Push notifications + Please, confirm push notifications that you want to receive. + You can enable or disable these notifications later in settings (Notifications tab). + + + Clear cache + There are %1$s of data in cache.\n\nWould you like to delete them? + Mb + Cache was cleared! %1$s were released + + Title + Title… + Description + Keywords + Keywords… + + Synchronize + Filter + Your toots + Your notifications + Public + Unlisted + Private + Direct + Some keywords… + Show media + Show pinned + No matching result found! + Backup toots for %1$s + %1$s new toots have been imported + %1$s new notifications have been imported + + Dates descending + Dates ascending + + + No + Only + Both + + No toots were found in database. Please, use the synchronize button from the menu to retrieve them. + + Recorded data + Only basic information from accounts are stored on the device. + These data are strictly confidential and can only be used by the application. + Deleting the application immediately removes these data.\n + ⚠ Login and passwords are never stored. They are only used during a secure authentication (SSL) with an instance. + + Permissions: + - ACCESS_NETWORK_STATE: Used to detect if the device is connected to a WIFI network.\n + - INTERNET: Used for queries to an instance.\n + - WRITE_EXTERNAL_STORAGE: Used to store media or to move the app on a SD card.\n + - READ_EXTERNAL_STORAGE: Used to add media to toots.\n + - BOOT_COMPLETED: Used to start the notification service.\n + - WAKE_LOCK: Used during the notification service. + + API permissions: + - Read: Read data.\n + - Write: Post statuses and upload media for statuses.\n + - Follow: Follow, unfollow, block, unblock.\n\n + ⚠ These actions are carried out only when user requests them. + + Tracking and Libraries + The application does not use tracking tools (audience measurement, error reporting, etc.) and does not contain any advertising.\n\n + The use of libraries is minimized: \n + - Glide: To manage media\n + - Android-Job: To manage services\n + - PhotoView: To manage images\n + + Translation of toots + The application offers the ability to translate toots using the locale of the device and the Yandex API.\n + Yandex has its proper privacy-policy which can be found here: https://yandex.ru/legal/confidential/?lang=en + + Thank you to: + + Filter out by regular expressions + Search + Delete + Fetch more toots… + + Lists + Are you sure you want to permanently delete this list? + There is nothing in this list yet. When members of this list post new statuses, they will appear here. + Add to list + Add list + Delete list + Edit list + New list title + The account was added to the list! + You don\'t have any lists yet! + + %1$s has moved to %2$s + Authentication does not work? + Here are some checks that might help:\n\n + - Check there is no spelling mistakes in the instance name\n\n + - Check that your instance is not down\n\n + - If you use the two-factor authentication (2FA), please use the link at the bottom (once the instance name is filled)\n\n + - You can also use this link without using the 2FA\n\n + - If it still does not work, please raise an issue on FramaGit at https://framagit.org/tom79/fedilab/issues + + Media has been loaded. Tap here to display it. + This action can be quite long. You will be notified when it will be finished. + Still running, please wait… + Export statuses + Export statuses for %1$s + %1$s toots out of %2$s have been exported. + Something went wrong when exporting data for %1$s + Something went wrong when exporting data! + Something went wrong when importing data! + + Proxy + Enable proxy? + Host + Port + Login + Password + Add toot details when sharing + Support the app on Liberapay + There is an error in the regular expression! + No timelines was found on this instance! + Delete this instance? + Translate in + Follow instance + You already follow this instance! + The instance is followed! + Partnerships + Information + Hide boosts from %s + Feature on profile + Show boosts from %s + Don\'t feature on profile + The account is now featured on profile + The account is no longer featured on profile + Boosts are now shown! + Boosts are now hidden! + Direct message + Filters + No filters to display. You can create one by tapping on the \"+\" button. + Keyword or phrase + Home timeline + Public timelines + Notifications + Conversations + Will be matched regardless of casing in text or content warning of a toot + Drop instead of hide + Filtered toots will disappear irreversibly, even if filter is later removed + When the keyword or phrase is alphanumeric only, it will only be applied if it matches the whole word + Whole word + Filter contexts + One or multiple contexts where the filter should apply + Expire after + Delete filter? + Update filter + Create filter + Whom to follow + There is no accounts listed for the moment! + Follow + Select all + Unselect all + %s is followed! + Creating the list %s + Adding accounts to the list + Accounts were added to the list + Adding accounts to the list + You have not created a list yet. Tap on the \"+\" button to add a new one. + Who to follow + Trunk API + Account(s) can\'t be followed + Fetching remote account + Automatically expand hidden media + New follow + New Boost + New Favourite + New Mention + Poll Ended + New Toot + Toots Backup + New posts + Media Download + Change notification sound + Select Tone + Enable time slot + How To Videos + Fetching remote thread! + No blocked domains! + Unblock domain + Are you sure to unblock %s? + Are you sure to block %s?\n\nYou will not see any content from that domain in any public timeline or in your notifications. Your followers from that domain will be removed. + Blocked domains + Block domain + The domain is blocked + The domain is no longer blocked! + Fetching remote status + Comment + Peertube instance + Be the first to leave a comment on this video with the top right button! + %s views + Duration: %s + Add an instance + Comments are not enabled on this video! + Pick up a resolution + Peertube favourites + The video has been added to bookmarks! + The video has been removed from bookmarks! + There is no Peertube videos in your favourites! + Channel + Videos + Channels + Use Emoji One + Information + Display previews in all toots + New UX/UI designer + Display video previews + The account id has been copied in the clipboard! + Change the language + Default language + Truncate long toots + Truncate toots over \'x\' lines. Zero means disabled. + Display more + Display less + Manage tags + The tag already exists! + The tag has been stored! + The tag has been changed! + The tag has been removed! + Schedule boost + The boost is scheduled! + No scheduled boost to display! + Schedule boost.]]> + Art timeline + Open menu + Go back + Logo of the application + Profile picture + Profile banner + Contact admin of the instance + Add new + MastoHost logo + Emoji picker + Refresh + Expand the conversation + Remove an account + Remove the blocked domain + Custom emoji picker + Play video + New toot + Image of the card + Hide media + Favicon + Add description for media (for the visually impaired) + + Never + 30 minutes + 1 hour + 6 hours + 12 hours + 1 day + 1 week + + In this field, you need to write your instance host name.\nFor example, if you created your account on https://mastodon.social\nJust write mastodon.social (without https://)\n + You can start writing first letters and names will be suggested.\n\n + ⚠ The Login button will only work if the instance name is valid and the instance is up! + + More information + + Languages + Media only + Show NSFW + Crowdin translations + Crowdin manager + Translation of the application + About Crowdin + Bot + Pixelfed instance + Mastodon instance + Any of these + All of these + None of these + Any of these words (space-separated) + All these words (space-separated) + Add some words to filter (space-separated) + Change column name + Misskey instance + No app supporting this link is installed on your device. + Subscriptions + Overview + Trending + Recently added + Local + Upload + Reply + Delete a comment + Are you sure to delete this comment? + Full screen video + Mode for videos + Select the file to upload + My videos + Title + License + Category + Language + This video contains mature or explicit content + Enable video comments + Update video + Description + The video has been updated! + Upload cancelled! + The video has been uploaded! + Uploading, please wait… + Tap here to edit the video data. + Delete video + Are you sure to delete this video? + Display NSFW videos + No videos to display! + Leave a comment + Share + Choose a schedule mode + From device + From server + Toots (Server) + Toots (Device) + Modify + Display new toots above the \"Fetch more\" button + Timelines + Interface + Contacts + %1$s commented your video %2$s]]> + %1$s is following your channel %2$s]]> + %1$s is following your account]]> + %1$s has been published]]> + %1$s succeeded]]> + %1$s failed]]> + %1$s published a new video: %2$s]]> + %1$s has been blacklisted]]> + %1$s has been unblacklisted]]> + Export data + Import Data + Select the file to import + An error occurred when selecting the backup file! + Add a public comment + Send comment + There is no Internet connection. Your message has been stored in drafts. + Plain text + HTML + Markdown + Logout account + All + Support the app + Open Collective enables groups to quickly set up a collective, raise funds and manage them transparently. + Copy link + Connect + Normal + Compact + Console + Set display mode + Patch the Security Provider + Update tracking domains + The tracking data base has been updated! + http calls blocked by the application + List of blocked calls + Submit + The data base has been exported! + Featured hashtags + Filter timeline with tags + No tags + Hide the \"delete\" button in the notification tab + Attach an image when sharing a URL + + Poll + Polls + Create a poll + Choice 1 + Choice 2 + Choice %d + You need two choices at least for the poll! + Done + end at %s + Refresh poll + Vote + A poll you have voted in has ended + A poll you tooted has ended + Customize + Categories + Time slot + Advanced + Display \'new\' badge on unread toots + Peertube + Move timeline + Hide timeline + Reorder timelines + List permanently deleted + Followed instance removed + Pinned tag removed + Undo + You need to keep two visible tabs! + Reorder timelines + Main timelines can only be hidden! + BBCode + Always mark media as sensitive + GNU instance + Cached status + Forward tags in replies + Long press to store media + Blur sensitive media + Display timelines in a list + Display timelines + Mark bot accounts in toots + Manage tags + Remember the position in Home timeline + History + Playlists + Display name + You don\'t have any playlists. Tap on the \"+\" icon to add a new playlist + You must provide a display name! + The channel is required when the playlist is public. + Create a playlist + There is nothing in this playlist yet. + redo + Gallery + Emoji + Sticker + Eraser + Text + Filter + Brush + Are you sure you want to exit without saving the image? + Discard + Saving… + Image Saved Successfully! + Failed to save Image + Opacity + Enable photo editor + Add a poll item + Remove last poll item + Mute conversation + Unmute conversation + The conversation is no longer muted! + The conversation is muted + Open application features + Timed mute + Mention the account + Refresh cache + Mention the status + News + General + Regional + Art + Journalism + Activism + Gaming + Technology + Adult content + Furry + Food + Logo of the instance + Something went wrong when checking available instances! + Join Mastodon + Choose an instance by picking up a category, then tap on a check button. + Choose an instance by tapping on a check button. + %1$s users + Confirm password + I agree to %1$s and %2$s + server rules + terms of service + Sign up + This instance works with invitations. Your account will need to be manually approved by an administrator before being usable. + Please, fill all the fields! + Passwords don\'t match! + The email doesn\'t seem to be valid! + Your username will be unique on %1$s + You will be sent a confirmation e-mail + Use at least 8 characters + Password should contain at least 8 characters + Username should only contain letters, numbers and underscores + Account created! + Your account has been created!\n\n + Think to validate your email within the 48 next hours.\n\n + You can now connect your account by writing %1$s in the first field and tap on Connect.\n\n + Important: If your instance required validation, you will receive an email once it is validated! + + Save the message in drafts? + Administration + Reports + No reports to display! + Reconnect the account + The application failed to access the admin features. You might need to reconnect the account for having the correct scope. + Unresolved + Remote + Active + Pending + Disabled + Silenced + Suspended + Permissions + Email status + Login status + Joined + Most recent IP + Warn + Disable + Silence + Notify the user per e-mail + Custom warning + User + Moderator + Administrator + Confirmed + Not confirmed + Reported statuses + Account + Undo silence + Undo disable + Suspend + Undo suspend + The account is silenced! + The account is no longer silenced! + The account is suspended! + The account is no longer suspended! + The account is disabled! + The account is no longer disabled! + The account has been warned! + Display the admin menu + Display the admin feature in statuses + Allow + The account is approved! + The account is rejected! + Assign to me + Unassign + Mark as resolved + Mark as unresolved + Empty content! + Display Fedilab features button + The application needs to access audio recording + Voice message + Enable quick reply + The account you are replying might not see your message! + If disabled, the app will always load last statuses + If disabled, sensitive media will be hidden with a button + Store media in full size with a long press on previews + Add an ellipse button at the top right for listing all tags/instances/lists + During the time slot, the app will send notifications. You can reverse (ie: silent) this time slot with the right spinner. + Display a Fedilab button below profile picture. It is a shortcut for accessing in-app features. + Allow to reply directly in timelines below statuses + Previews will not be cropped in timelines + Allow to play embedded videos directly in timelines + Allow to reverse the way to read statuses that are displayed once tapping the fetch more button + This option allows to support recent cipher suites. It is useful for older Android devices or if you cannot connect to your instance. + Exclusively for Peertube videos. Switch this mode if you cannot play them. + These tags will allow to filter statuses from profiles. You will have to use the context menu for seeing them. + Automatically insert a line break after the mention to capitalize the first letter + Allow content creators to share statuses to their RSS feeds + Compose + Maximum retry times when uploading media + Create a new Folder here + Enter the folder name + Please enter a valid folder name + This folder already exists.\n Please provide another name for the folder + Select + Default Directory + Folder + Create folder + Display a toast message after an action has been completed (boost, fav, etc.)? + Muted instances have been exported! + Add an instance + Export instances + Import instances + Crash reports + Enable crash reports + If enabled, a crash report will be created locally and then you will be able to share it. + Fedilab has stopped :( + You can send me by email the crash report. It will help to fix it :)\n\nYou can add additional content. Thank you! + Use the wysiwyg + When enabled, you will be able to format your text easily with tools. + Statistics + Total statuses + Number of boosts + Number of favourites + Number of mentions + Number of follows + Number of polls + Number of replies + Number of statuses + Statuses + Visibility + Number with media + Number with sensitive media + Number with CW + First status date + Last status date + First notification date + Last notification date + Frequency + %s statuses per day + %s notifications per day + Date range + Groups + No groups! + Disable custom animated emojis + Charts + Display charts + The application collects your local data, please wait… + Backup + Auto backup statuses + This option is per account. It will launch a service that will automatically store your statuses locally in the database. That allows to get statistics and charts + Auto backup notifications + This option is per account. It will launch a service that will automatically store your notifications locally in the database. That allows to get statistics and charts + Report account + Send an invitation + Your instance does not allow to register a new account! + + %d vote + %d votes + + + %d voter + %d voters + + + Single choice + Multiple choices + + + 5 minutes + 30 minutes + 1 hour + 6 hours + 1 day + 3 days + 7 days + + + Webview + Direct stream + + For joining my instance \"%1$s\", you can download Fedilab:\n\nF-Droid: %2$s\nGoogle: %3$s\n\nThen open the link below with Fedilab and create your account :)\n\n%4$s + + Your poll can\'t have duplicated options! + For all accounts + Database cache + Clear your home timeline cache + Clear your cached statuses + Clear your bookmarks + Files in cache + Total notifications + Hide menu items + Fedilab is running live notifications + For %1$s accounts with %2$s events + Live notifications for %1$s + Live notifications will be enabled for this account. + Clear cache when leaving + The cache (media, cached messages, data from the built-in browser) will be automatically cleared when leaving the application. + Do you want to unfollow this account? + Show confirmation dialog before unfollowing + Replace Youtube with Invidio.us + Invidious is an alternative front-end to YouTube + Enter your custom host or leave blank for using invidious.snopyta.org + Replace Twitter with Nitter + Nitter is an open source alternative Twitter front-end focused on privacy. + Enter your custom host or leave blank for using nitter.net + Replace Instagram with Bibliogram + Bibliogram is an open source alternative Instagram front-end focused on privacy. + Enter your custom host or leave blank for using bibliogram.art + Replace Reddit with Libreddit + Libreddit is an open source alternative Reddit front-end focused on privacy. + Enter your custom host or leave blank for using libredd.it + Replace Medium links + Replace medium.com links with an open source alternative front-end focused on privacy. + Default: scribe.rip + Replace Wikipedia links + Replace Wikipedia link with an open source alternative front-end focused on privacy. + Default: wikiless.org + Hide Fedilab notification bar + For hiding the remaining notification in the status bar, tap on the eye icon button then uncheck: \"Display in status bar\" + Use a push notifications system for getting notifications in real time. + No live notifications + Live notifications + Notifications will be fetched every 15 minutes. + Add notes + Notes for the account + Allow to compress large photos into smaller sized photos with very less or negligible loss in quality of the image. + Allow compressing videos while maintaining their quality. + The app is compressing the media, it can be quite long… + Change app icon + Tap to change the app icon + Post + Visibility of the post + Tap here to add photos + Accepted Formats: jpeg, png, gif \n\nMax File Size: 15 MB \n\nAlbums can contain up to 4 photos or videos + Upload media + Add an optional caption + The app received a very long error message from the API %1$s + Message preview + Add mentions in each message + Fetching conversation + Order by + Title for the video + Join Peertube + I am at least 16 years old and agree to the %1$s of this instance + Links + Change the color of links (URLs, mentions, tags, etc.) in messages + Reblogs header + Change the color of display name at the top of messages + Change the color of the user name at the top of messages + Change the color of the header for reblogs + Posts + Background color of posts in timelines + Reset colors + Tap here to reset all your custom colors + Reset + Icons + Color of bottom icons in timelines + Pin this tag + Logo of the instance + Edit profile + Make an action + Translation + Image preview + Text color + Change the text color in pots + Apply changes + You need to restart the application to apply changes + Restart + Use a custom theme + Allow to override colors of the selected theme above + Theming + Store before + The theme was exported + The theme has been successfully exported in CSV + Apply the primary color to the status bar + Status bar color + Restore a default theme + Import a theme + Tap here to import a theme from a previous export + Export the theme + Tap here to export the current theme + An error occurred when selecting the theme file + Theme Picker + Select a pre-installed theme + Themes + Apply the primary color to the navigation bar + Navigation bar color + The underlying color of the app’s content. + Background color + Accents select parts of the UI. + Accent color + Displayed most frequently across your app. + Primary color + Export bookmarks to the instance + Import bookmarks from the instance + User count + Status count + Instance count + Blocked + End in %s + What\'s new in %s + You can follow my account for updates + This instance is not available on https://instances.social + Display full link + Share link + The URL has been copied to the clipboard + Open with another app + Check redirect + This URL does not redirect + %1$s \n\nredirects to\n\n %2$s + Change the user agent + Set a custom user agent or leave blank + Allows to customize the user agent used for api calls or with the built-in browser. + Remove UTM parameters + The app will automatically remove UTM parameters from URLs before visiting a link. + Trends + Trending now + %d people talking + Twitter accounts (via Nitter) + Twitter usernames space separated + Identity proofs + Verified identity + Verified by %1$s (%2$s) + Delete the notification + Display more options + It is a Pixelfed story + Upload a media, it will be automatically added to your Pixelfed story. + Media successfully added to your story! + Action disabled + Unfollow + Something went wrong, please check your download directory in settings. + Announcements + No announcements! + Add a reaction + Use your favourite browser inside the app. Uncheck this feature to open links externally. + Video cache in MB, zero means no cache. + Watermarks + Automatically add a watermark at the bottom of pictures. The text can be customized for each account. + No distributors found! + You need a distributor for receiving push notifications.\nYou will find more details at %1$s.\n\nYou can also disable push notifications in settings for ignoring that message. + Select a distributor + diff --git a/app/src/main/res/values-br/strings.xml b/app/src/main/res/values-br/strings.xml new file mode 100644 index 000000000..627331095 --- /dev/null +++ b/app/src/main/res/values-br/strings.xml @@ -0,0 +1,1166 @@ + + + Open the menu + Close the menu + About + About the instance + Privacy + Cache + Logout + Login + + Close + Yes + No + Cancel + Download + Download %1$s + Media saved + File: %1$s + Password + Email + Accounts + Toots + Tags + Save + Restore + No results! + Instance + Instance: mastodon.social + Now works with the account %1$s + Add an account + The content of the toot has been copied to the clipboard + The URL of the toot has been copied to the clipboard + Change + Select a picture… + Clean + Camera + Delete all + Translate this toot. + Schedule + Text and icon sizes + Change the current text size: + Change the current icon size: + Next + Previous + Open with + Validate + Media + Share with + Shared via Fedilab + Replies + User name + Drafts + Favourites + New followers + Mentions + Boosts + Show boosts + Show replies + Open in browser + Translate + Please, wait few seconds before making this action. + + Home + Local timeline + Federated timeline + Options + Favourites + Communication + Muted users + Blocked users + Notifications + Follow requests + Settings + Remove an account + Remove the account %1$s from the application? + Send an email + Tap on the path to change it + Failed! + Scheduled toots + Information below may reflect the user\'s profile incompletely. + Insert emoji + The app did not collect custom emojis for the moment. + Push notifications + Are you sure you want to logout? + Are you sure you want to logout @%1$s@%2$s? + + No toot to display + No stories to display + Stories + Boosted by %1$s + Add this toot to your favourites? + Remove this toot from your favourites? + Boost this toot? + Unboost this toot? + Pin this toot? + Unpin this toot? + Mute + Block + Report + Delete + Copy + Share + Mention + Timed mute + Delete & re-draft + + Mute this account? + Block this account? + Report this toot? + Block this domain? + Unmute this account? + Unblock this account? + + + Notify + Silent + + + Delete this toot? + Delete & re-draft this toot? + + Bookmarks + Add to bookmarks + Remove bookmark + No bookmarks to display + Status has been added to bookmarks! + Status was removed from bookmarks! + + %d s + %d m + %d h + %d d + + %d second + %d seconds + %d seconds + %d seconds + %d seconds + + + %d minute + %d minutes + %d minutes + %d minutes + %d minutes + + + %d hour + %d hours + %d hours + %d hours + %d hours + + + %d day + %d days + %d days + %d days + %d days + + + Warning + What is on your mind? + TOOT! + QUEET! + cw + Write a toot + Reply to a toot + Write a queet + Reply to a queet + Select a media + An error occurred while selecting the media! + Remove this media? + Your toot is empty! + Visibility of the toot + Visibility of the toots by default: + The toot has been sent! + You are replying to this toot: + Sensitive content? + + Post to public timelines + Do not post to public timelines + Post to followers only + Post to mentioned users only + + No drafts! + Choose a toot + Choose an account + Select some accounts + Delete draft? + Tap on the button to display the original toot + Describe for the visually impaired + + No description available! + + Release %1$s + Developer: + License: + GNU GPL V3 + Source code: + Translation of toots: + Search instances: + Icon designer: + + Conversation + + No account to display + No follow request + Toots \n %1$s + Following \n %1$s + Followers \n %1$s + Pinned \n %d + Authorize + Reject + + No scheduled toots to display! + Write a toot and then choose Schedule from the top menu. + Delete scheduled toot? + Media: %d + The toot has been scheduled! + The scheduled date must be greater than the current hour! + Battery saver is enabled! It might not work as expected. + + The time for muting should be greater than one minute. + %1$s has been muted until %2$s.\n You can unmute this account from their profile page. + %1$s is muted until %2$s.\n Tap here to unmute the account. + + No notification to display + mentioned you + wrote a new message + boosted your status + favourited your status + followed you + asked to follow you + + and another notification + and %d other notifications + and %d other notifications + and %d other notifications + and %d other notifications + + + %d like + %d likes + %d likes + %d likes + %d likes + + Delete a notification? + Delete all notifications? + The notification has been deleted! + All notifications have been deleted! + + Following + Followers + Pinned + + Unable to get client id! + Unable to connect to instance domain! + No Internet connection! + The account was blocked! + The account is no longer blocked! + The account was muted! + The account is no longer muted! + The account was followed! + The account is no longer followed! + The toot was boosted! + The toot is no longer boosted! + The toot was added to your favourites! + The toot was removed from your favourites! + The toot was reported! + The toot was deleted! + The toot was pinned! + The toot was unpinned! + Oops ! An error occurred! + An error occurred! The instance did not return an authorisation code! + The instance domain does not seem to be valid! + An error occurred while switching between accounts! + An error occurred while searching! + The profile data have been saved! + No action can be taken + The media has been saved! + An error occurred while translating! + Translations are disabled in settings + Draft saved! + Are you sure this instance allows this number of characters? Usually, this value is close to 500 characters. + Visibility of the toots has been changed for the account %1$s + + Number of toots per load + Always + WIFI + Ask + Load the media + Load the pictures + Show more… + Show less… + Sensitive content + Disable GIF avatars + Path: + Save drafts automatically + Add URL of media in toots + Notify when someone follows you + Notify when someone boosts your status + Notify when someone favourites your status + Notify when someone mentions you + Notify when a poll ended + Notify for new posts + Show confirmation dialog before boosting + Show confirmation dialog before adding to favourites + Notify in WIFI only + Notify? + Silent Notifications + NSFW view timeout (seconds, 0 means off) + Media Description timeout (seconds, 0 means off) + Edit profile + Custom sharing + Your custom sharing URL… + Bio… + Lock account + Save changes + Choose a header picture + Fit preview images + Automatically split toots in replies when chars are over: + You have reached the 160 characters allowed! + You have reached the 30 characters allowed! + Between + and + The time must be greater than %1$s + The time must be lower than %1$s + Start time + End time + Use the built-in browser + Custom tabs + Enable Javascript + Automatically expand cw + Allow third-party cookies + Your API key, you can leave blank for Yandex + + Dark + Light + Black + + Set LED colour: + + Blue + Cyan + Magenta + Green + Red + Yellow + White + + Follow + Unblock + Mute + Unmute + Request sent + Follows you + Search + First letter in capital for replies + Resize pictures + Resize videos + + Push notifications + Please, confirm push notifications that you want to receive. + You can enable or disable these notifications later in settings (Notifications tab). + + + Clear cache + There are %1$s of data in cache.\n\nWould you like to delete them? + Mb + Cache was cleared! %1$s were released + + Title + Title… + Description + Keywords + Keywords… + + Synchronize + Filter + Your toots + Your notifications + Public + Unlisted + Private + Direct + Some keywords… + Show media + Show pinned + No matching result found! + Backup toots for %1$s + %1$s new toots have been imported + %1$s new notifications have been imported + + Dates descending + Dates ascending + + + No + Only + Both + + No toots were found in database. Please, use the synchronize button from the menu to retrieve them. + + Recorded data + Only basic information from accounts are stored on the device. + These data are strictly confidential and can only be used by the application. + Deleting the application immediately removes these data.\n + ⚠ Login and passwords are never stored. They are only used during a secure authentication (SSL) with an instance. + + Permissions: + - ACCESS_NETWORK_STATE: Used to detect if the device is connected to a WIFI network.\n + - INTERNET: Used for queries to an instance.\n + - WRITE_EXTERNAL_STORAGE: Used to store media or to move the app on a SD card.\n + - READ_EXTERNAL_STORAGE: Used to add media to toots.\n + - BOOT_COMPLETED: Used to start the notification service.\n + - WAKE_LOCK: Used during the notification service. + + API permissions: + - Read: Read data.\n + - Write: Post statuses and upload media for statuses.\n + - Follow: Follow, unfollow, block, unblock.\n\n + ⚠ These actions are carried out only when user requests them. + + Tracking and Libraries + The application does not use tracking tools (audience measurement, error reporting, etc.) and does not contain any advertising.\n\n + The use of libraries is minimized: \n + - Glide: To manage media\n + - Android-Job: To manage services\n + - PhotoView: To manage images\n + + Translation of toots + The application offers the ability to translate toots using the locale of the device and the Yandex API.\n + Yandex has its proper privacy-policy which can be found here: https://yandex.ru/legal/confidential/?lang=en + + Thank you to: + + Filter out by regular expressions + Search + Delete + Fetch more toots… + + Lists + Are you sure you want to permanently delete this list? + There is nothing in this list yet. When members of this list post new statuses, they will appear here. + Add to list + Add list + Delete list + Edit list + New list title + The account was added to the list! + You don\'t have any lists yet! + + %1$s has moved to %2$s + Authentication does not work? + Here are some checks that might help:\n\n + - Check there is no spelling mistakes in the instance name\n\n + - Check that your instance is not down\n\n + - If you use the two-factor authentication (2FA), please use the link at the bottom (once the instance name is filled)\n\n + - You can also use this link without using the 2FA\n\n + - If it still does not work, please raise an issue on FramaGit at https://framagit.org/tom79/fedilab/issues + + Media has been loaded. Tap here to display it. + This action can be quite long. You will be notified when it will be finished. + Still running, please wait… + Export statuses + Export statuses for %1$s + %1$s toots out of %2$s have been exported. + Something went wrong when exporting data for %1$s + Something went wrong when exporting data! + Something went wrong when importing data! + + Proxy + Enable proxy? + Host + Port + Login + Password + Add toot details when sharing + Support the app on Liberapay + There is an error in the regular expression! + No timelines was found on this instance! + Delete this instance? + Translate in + Follow instance + You already follow this instance! + The instance is followed! + Partnerships + Information + Hide boosts from %s + Feature on profile + Show boosts from %s + Don\'t feature on profile + The account is now featured on profile + The account is no longer featured on profile + Boosts are now shown! + Boosts are now hidden! + Direct message + Filters + No filters to display. You can create one by tapping on the \"+\" button. + Keyword or phrase + Home timeline + Public timelines + Notifications + Conversations + Will be matched regardless of casing in text or content warning of a toot + Drop instead of hide + Filtered toots will disappear irreversibly, even if filter is later removed + When the keyword or phrase is alphanumeric only, it will only be applied if it matches the whole word + Whole word + Filter contexts + One or multiple contexts where the filter should apply + Expire after + Delete filter? + Update filter + Create filter + Whom to follow + There is no accounts listed for the moment! + Follow + Select all + Unselect all + %s is followed! + Creating the list %s + Adding accounts to the list + Accounts were added to the list + Adding accounts to the list + You have not created a list yet. Tap on the \"+\" button to add a new one. + Who to follow + Trunk API + Account(s) can\'t be followed + Fetching remote account + Automatically expand hidden media + New follow + New Boost + New Favourite + New Mention + Poll Ended + New Toot + Toots Backup + New posts + Media Download + Change notification sound + Select Tone + Enable time slot + How To Videos + Fetching remote thread! + No blocked domains! + Unblock domain + Are you sure to unblock %s? + Are you sure to block %s?\n\nYou will not see any content from that domain in any public timeline or in your notifications. Your followers from that domain will be removed. + Blocked domains + Block domain + The domain is blocked + The domain is no longer blocked! + Fetching remote status + Comment + Peertube instance + Be the first to leave a comment on this video with the top right button! + %s views + Duration: %s + Add an instance + Comments are not enabled on this video! + Pick up a resolution + Peertube favourites + The video has been added to bookmarks! + The video has been removed from bookmarks! + There is no Peertube videos in your favourites! + Channel + Videos + Channels + Use Emoji One + Information + Display previews in all toots + New UX/UI designer + Display video previews + The account id has been copied in the clipboard! + Change the language + Default language + Truncate long toots + Truncate toots over \'x\' lines. Zero means disabled. + Display more + Display less + Manage tags + The tag already exists! + The tag has been stored! + The tag has been changed! + The tag has been removed! + Schedule boost + The boost is scheduled! + No scheduled boost to display! + Schedule boost.]]> + Art timeline + Open menu + Go back + Logo of the application + Profile picture + Profile banner + Contact admin of the instance + Add new + MastoHost logo + Emoji picker + Refresh + Expand the conversation + Remove an account + Remove the blocked domain + Custom emoji picker + Play video + New toot + Image of the card + Hide media + Favicon + Add description for media (for the visually impaired) + + Never + 30 minutes + 1 hour + 6 hours + 12 hours + 1 day + 1 week + + In this field, you need to write your instance host name.\nFor example, if you created your account on https://mastodon.social\nJust write mastodon.social (without https://)\n + You can start writing first letters and names will be suggested.\n\n + ⚠ The Login button will only work if the instance name is valid and the instance is up! + + More information + + Languages + Media only + Show NSFW + Crowdin translations + Crowdin manager + Translation of the application + About Crowdin + Bot + Pixelfed instance + Mastodon instance + Any of these + All of these + None of these + Any of these words (space-separated) + All these words (space-separated) + Add some words to filter (space-separated) + Change column name + Misskey instance + No app supporting this link is installed on your device. + Subscriptions + Overview + Trending + Recently added + Local + Upload + Reply + Delete a comment + Are you sure to delete this comment? + Full screen video + Mode for videos + Select the file to upload + My videos + Title + License + Category + Language + This video contains mature or explicit content + Enable video comments + Update video + Description + The video has been updated! + Upload cancelled! + The video has been uploaded! + Uploading, please wait… + Tap here to edit the video data. + Delete video + Are you sure to delete this video? + Display NSFW videos + No videos to display! + Leave a comment + Share + Choose a schedule mode + From device + From server + Toots (Server) + Toots (Device) + Modify + Display new toots above the \"Fetch more\" button + Timelines + Interface + Contacts + %1$s commented your video %2$s]]> + %1$s is following your channel %2$s]]> + %1$s is following your account]]> + %1$s has been published]]> + %1$s succeeded]]> + %1$s failed]]> + %1$s published a new video: %2$s]]> + %1$s has been blacklisted]]> + %1$s has been unblacklisted]]> + Export data + Import Data + Select the file to import + An error occurred when selecting the backup file! + Add a public comment + Send comment + There is no Internet connection. Your message has been stored in drafts. + Plain text + HTML + Markdown + Logout account + All + Support the app + Open Collective enables groups to quickly set up a collective, raise funds and manage them transparently. + Copy link + Connect + Normal + Compact + Console + Set display mode + Patch the Security Provider + Update tracking domains + The tracking data base has been updated! + http calls blocked by the application + List of blocked calls + Submit + The data base has been exported! + Featured hashtags + Filter timeline with tags + No tags + Hide the \"delete\" button in the notification tab + Attach an image when sharing a URL + + Poll + Polls + Create a poll + Choice 1 + Choice 2 + Choice %d + You need two choices at least for the poll! + Done + end at %s + Refresh poll + Vote + A poll you have voted in has ended + A poll you tooted has ended + Customize + Categories + Time slot + Advanced + Display \'new\' badge on unread toots + Peertube + Move timeline + Hide timeline + Reorder timelines + List permanently deleted + Followed instance removed + Pinned tag removed + Undo + You need to keep two visible tabs! + Reorder timelines + Main timelines can only be hidden! + BBCode + Always mark media as sensitive + GNU instance + Cached status + Forward tags in replies + Long press to store media + Blur sensitive media + Display timelines in a list + Display timelines + Mark bot accounts in toots + Manage tags + Remember the position in Home timeline + History + Playlists + Display name + You don\'t have any playlists. Tap on the \"+\" icon to add a new playlist + You must provide a display name! + The channel is required when the playlist is public. + Create a playlist + There is nothing in this playlist yet. + redo + Gallery + Emoji + Sticker + Eraser + Text + Filter + Brush + Are you sure you want to exit without saving the image? + Discard + Saving… + Image Saved Successfully! + Failed to save Image + Opacity + Enable photo editor + Add a poll item + Remove last poll item + Mute conversation + Unmute conversation + The conversation is no longer muted! + The conversation is muted + Open application features + Timed mute + Mention the account + Refresh cache + Mention the status + News + General + Regional + Art + Journalism + Activism + Gaming + Technology + Adult content + Furry + Food + Logo of the instance + Something went wrong when checking available instances! + Join Mastodon + Choose an instance by picking up a category, then tap on a check button. + Choose an instance by tapping on a check button. + %1$s users + Confirm password + I agree to %1$s and %2$s + server rules + terms of service + Sign up + This instance works with invitations. Your account will need to be manually approved by an administrator before being usable. + Please, fill all the fields! + Passwords don\'t match! + The email doesn\'t seem to be valid! + Your username will be unique on %1$s + You will be sent a confirmation e-mail + Use at least 8 characters + Password should contain at least 8 characters + Username should only contain letters, numbers and underscores + Account created! + Your account has been created!\n\n + Think to validate your email within the 48 next hours.\n\n + You can now connect your account by writing %1$s in the first field and tap on Connect.\n\n + Important: If your instance required validation, you will receive an email once it is validated! + + Save the message in drafts? + Administration + Reports + No reports to display! + Reconnect the account + The application failed to access the admin features. You might need to reconnect the account for having the correct scope. + Unresolved + Remote + Active + Pending + Disabled + Silenced + Suspended + Permissions + Email status + Login status + Joined + Most recent IP + Warn + Disable + Silence + Notify the user per e-mail + Custom warning + User + Moderator + Administrator + Confirmed + Not confirmed + Reported statuses + Account + Undo silence + Undo disable + Suspend + Undo suspend + The account is silenced! + The account is no longer silenced! + The account is suspended! + The account is no longer suspended! + The account is disabled! + The account is no longer disabled! + The account has been warned! + Display the admin menu + Display the admin feature in statuses + Allow + The account is approved! + The account is rejected! + Assign to me + Unassign + Mark as resolved + Mark as unresolved + Empty content! + Display Fedilab features button + The application needs to access audio recording + Voice message + Enable quick reply + The account you are replying might not see your message! + If disabled, the app will always load last statuses + If disabled, sensitive media will be hidden with a button + Store media in full size with a long press on previews + Add an ellipse button at the top right for listing all tags/instances/lists + During the time slot, the app will send notifications. You can reverse (ie: silent) this time slot with the right spinner. + Display a Fedilab button below profile picture. It is a shortcut for accessing in-app features. + Allow to reply directly in timelines below statuses + Previews will not be cropped in timelines + Allow to play embedded videos directly in timelines + Allow to reverse the way to read statuses that are displayed once tapping the fetch more button + This option allows to support recent cipher suites. It is useful for older Android devices or if you cannot connect to your instance. + Exclusively for Peertube videos. Switch this mode if you cannot play them. + These tags will allow to filter statuses from profiles. You will have to use the context menu for seeing them. + Automatically insert a line break after the mention to capitalize the first letter + Allow content creators to share statuses to their RSS feeds + Compose + Maximum retry times when uploading media + Create a new Folder here + Enter the folder name + Please enter a valid folder name + This folder already exists.\n Please provide another name for the folder + Select + Default Directory + Folder + Create folder + Display a toast message after an action has been completed (boost, fav, etc.)? + Muted instances have been exported! + Add an instance + Export instances + Import instances + Crash reports + Enable crash reports + If enabled, a crash report will be created locally and then you will be able to share it. + Fedilab has stopped :( + You can send me by email the crash report. It will help to fix it :)\n\nYou can add additional content. Thank you! + Use the wysiwyg + When enabled, you will be able to format your text easily with tools. + Statistics + Total statuses + Number of boosts + Number of favourites + Number of mentions + Number of follows + Number of polls + Number of replies + Number of statuses + Statuses + Visibility + Number with media + Number with sensitive media + Number with CW + First status date + Last status date + First notification date + Last notification date + Frequency + %s statuses per day + %s notifications per day + Date range + Groups + No groups! + Disable custom animated emojis + Charts + Display charts + The application collects your local data, please wait… + Backup + Auto backup statuses + This option is per account. It will launch a service that will automatically store your statuses locally in the database. That allows to get statistics and charts + Auto backup notifications + This option is per account. It will launch a service that will automatically store your notifications locally in the database. That allows to get statistics and charts + Report account + Send an invitation + Your instance does not allow to register a new account! + + %d vote + %d votes + %d votes + %d votes + %d votes + + + %d voter + %d voters + %d voters + %d voters + %d voters + + + Single choice + Multiple choices + + + 5 minutes + 30 minutes + 1 hour + 6 hours + 1 day + 3 days + 7 days + + + Webview + Direct stream + + For joining my instance \"%1$s\", you can download Fedilab:\n\nF-Droid: %2$s\nGoogle: %3$s\n\nThen open the link below with Fedilab and create your account :)\n\n%4$s + + Your poll can\'t have duplicated options! + For all accounts + Database cache + Clear your home timeline cache + Clear your cached statuses + Clear your bookmarks + Files in cache + Total notifications + Hide menu items + Fedilab is running live notifications + For %1$s accounts with %2$s events + Live notifications for %1$s + Live notifications will be enabled for this account. + Clear cache when leaving + The cache (media, cached messages, data from the built-in browser) will be automatically cleared when leaving the application. + Do you want to unfollow this account? + Show confirmation dialog before unfollowing + Replace Youtube with Invidio.us + Invidious is an alternative front-end to YouTube + Enter your custom host or leave blank for using invidious.snopyta.org + Replace Twitter with Nitter + Nitter is an open source alternative Twitter front-end focused on privacy. + Enter your custom host or leave blank for using nitter.net + Replace Instagram with Bibliogram + Bibliogram is an open source alternative Instagram front-end focused on privacy. + Enter your custom host or leave blank for using bibliogram.art + Replace Reddit with Libreddit + Libreddit is an open source alternative Reddit front-end focused on privacy. + Enter your custom host or leave blank for using libredd.it + Replace Medium links + Replace medium.com links with an open source alternative front-end focused on privacy. + Default: scribe.rip + Replace Wikipedia links + Replace Wikipedia link with an open source alternative front-end focused on privacy. + Default: wikiless.org + Hide Fedilab notification bar + For hiding the remaining notification in the status bar, tap on the eye icon button then uncheck: \"Display in status bar\" + Use a push notifications system for getting notifications in real time. + No live notifications + Live notifications + Notifications will be fetched every 15 minutes. + Add notes + Notes for the account + Allow to compress large photos into smaller sized photos with very less or negligible loss in quality of the image. + Allow compressing videos while maintaining their quality. + The app is compressing the media, it can be quite long… + Change app icon + Tap to change the app icon + Post + Visibility of the post + Tap here to add photos + Accepted Formats: jpeg, png, gif \n\nMax File Size: 15 MB \n\nAlbums can contain up to 4 photos or videos + Upload media + Add an optional caption + The app received a very long error message from the API %1$s + Message preview + Add mentions in each message + Fetching conversation + Order by + Title for the video + Join Peertube + I am at least 16 years old and agree to the %1$s of this instance + Links + Change the color of links (URLs, mentions, tags, etc.) in messages + Reblogs header + Change the color of display name at the top of messages + Change the color of the user name at the top of messages + Change the color of the header for reblogs + Posts + Background color of posts in timelines + Reset colors + Tap here to reset all your custom colors + Reset + Icons + Color of bottom icons in timelines + Pin this tag + Logo of the instance + Edit profile + Make an action + Translation + Image preview + Text color + Change the text color in pots + Apply changes + You need to restart the application to apply changes + Restart + Use a custom theme + Allow to override colors of the selected theme above + Theming + Store before + The theme was exported + The theme has been successfully exported in CSV + Apply the primary color to the status bar + Status bar color + Restore a default theme + Import a theme + Tap here to import a theme from a previous export + Export the theme + Tap here to export the current theme + An error occurred when selecting the theme file + Theme Picker + Select a pre-installed theme + Themes + Apply the primary color to the navigation bar + Navigation bar color + The underlying color of the app’s content. + Background color + Accents select parts of the UI. + Accent color + Displayed most frequently across your app. + Primary color + Export bookmarks to the instance + Import bookmarks from the instance + User count + Status count + Instance count + Blocked + End in %s + What\'s new in %s + You can follow my account for updates + This instance is not available on https://instances.social + Display full link + Share link + The URL has been copied to the clipboard + Open with another app + Check redirect + This URL does not redirect + %1$s \n\nredirects to\n\n %2$s + Change the user agent + Set a custom user agent or leave blank + Allows to customize the user agent used for api calls or with the built-in browser. + Remove UTM parameters + The app will automatically remove UTM parameters from URLs before visiting a link. + Trends + Trending now + %d people talking + Twitter accounts (via Nitter) + Twitter usernames space separated + Identity proofs + Verified identity + Verified by %1$s (%2$s) + Delete the notification + Display more options + It is a Pixelfed story + Upload a media, it will be automatically added to your Pixelfed story. + Media successfully added to your story! + Action disabled + Unfollow + Something went wrong, please check your download directory in settings. + Announcements + No announcements! + Add a reaction + Use your favourite browser inside the app. Uncheck this feature to open links externally. + Video cache in MB, zero means no cache. + Watermarks + Automatically add a watermark at the bottom of pictures. The text can be customized for each account. + No distributors found! + You need a distributor for receiving push notifications.\nYou will find more details at %1$s.\n\nYou can also disable push notifications in settings for ignoring that message. + Select a distributor + diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml new file mode 100644 index 000000000..2223983ad --- /dev/null +++ b/app/src/main/res/values-ca/strings.xml @@ -0,0 +1,1132 @@ + + + Obre el menú + Tanca el menú + Quant a + Quant a la instància + Privadesa + Memòria cau + Tanca la sessió + Inicia la sessió + + Tanca + + No + Cancel·la + Baixa + Baixa %1$s + S\'ha desat l\'element multimèdia + Document: %1$s + Contrasenya + Adreça electrònica + Comptes + Brams + Etiquetes + Desa + Restaura + No hi ha cap resultat! + Instància + Instància: mastodon.social + Ara funciona amb el compte %1$s + Afegeix un compte + El contingut del bram s\'ha copiat al porta-retalls + L\'adreça web del bram s\'ha copiat al porta-retalls + Canvia + Selecciona una imatge… + Neteja + Càmera + Suprimeix-ho tot + Tradueix aquest bram. + Temporitza + Mides de text i icones + Canvia la mida del text actual: + Canvia la mida de la icona actual: + Següent + Anterior + Obre amb + Valida + Mèdia + Comparteix amb + Compartit via Fedilab + Respostes + Nom d\'usuària + Esborranys + Preferits + Noves seguidores + Mencions + Difusions + Mostrar difusions + Mostra les respostes + Obre al navegador + Tradueix + Si us plau, espereu uns segons abans de fer aquesta acció. + + Inici + Pissarra local + Pissarra federada + Opcions + Preferits + Comunicació + Usuàries silenciades + Usuàries blocades + Notificacions + Peticions de seguiment + Configuració + Suprimeix un compte + Voleu suprimir el compte %1$s de l\'aplicació? + Envia un correu electrònic + Feu clic en a la ruta per canviar-la + Ha fallat! + Brams temporitzats + Aquesta informació d\'usuària pot estar incompleta. + Inserir emoji + L\'app no ha recollit emojis personalitzats de moment. + Missatges de l\'aplicació + Estàs segur que vols tancar la sessió? + Estàs segura que vols tancar la sessió @%1$s@%2$s? + + Cap bram per mostrar + No hi ha històries per mostrar + Històries + Difós per %1$s + Afegir aquest bram als preferits? + Esborrar aquest bram dels preferits? + Vols difondre aquest bram? + Desdifondre el bram? + Vols fixar aquest bram? + Desfixar aquest bram? + Silencia + Bloca + Denuncia + Suprimeix + Copia + Comparteix + Menciona + Silenciar temporalment + Suprimir & tornar a redactar + + Vols silenciar aquest compte? + Vols blocar aquest compte? + Vols denunciar aquest bram? + Vols blocar el domini? + Desilenciar aquest compte? + Desbloquejar aquest compte? + + + Notificar + Silenciar + + + Vols suprimir aquest bram? + Suprimir & tornar a redactar aquest bram? + + Marcadors + Afegeix als marcadors + Suprimeix el marcador + No hi ha cap marcador per a mostrar + S\'ha afegit el missatge als marcadors! + S\'ha suprimit el missatge dels marcadors! + + %d s + %d m + %d h + %d d. + + %d segon + %d segons + + + %d minut + %d minuts + + + %d hora + %d hores + + + %d dia + %d dies + + + Alerta + Què et passa pel cap? + BRAMULA! + Quita! + ac + Escriu un bram + Respon a un toot + Escriu un quit + Respon a un quit + Escull un recurs multimèdia + S\'ha produït un error en seleccionar el mèdia! + Vols suprimir aquest mèdia? + El bram és buit! + Visibilitat del bram + Visibilitat predeterminada dels brams: + S\'ha enviat el bram! + Estàs responent a aquest bram: + És contingut sensible? + + Penja\'l a les pissarres públiques + No el pengis en pissarres públiques + Penja\'l només per als seguidors + Penja\'l només per a usuàries amb mencions + + No hi ha esborranys! + Escull un bram + Escull un compte + Seleccionar comptes + Vols suprimir l\'esborrany? + Fes clic al botó per mostrar el bram original + Descriu-ho per a disminuïts visuals + + No hi ha descripció disponible! + + Versió %1$s + Desenvolupador: + Llicència: + GNU GPL V3 + Codi font: + Traducció dels brams: + Cerca instàncies: + Disseny d\'icones: + + Conversa + + No hi ha comptes per mostrar + No hi ha peticions de seguiment + Brams \n %1$s + A qui segueixo \n %1$s + Seguidores \n %1$s + Fixats \n %d + Autoritza + Rebutja + + No hi ha brams programats per mostrar! + Escriu un bram i selecciona Temporització al menú de dalt. + Suprimir bram programat? + Mèdia: %d + S\'ha temporitzat el bram! + La data programada ha de ser posterior al moment actual! + L\'estalvi de bateria està activat! Això pot no rutllar com està previst. + + El silenciament ha de durar més d\'un minut. + %1$s s\'ha silenciat fins a %2$s.\n Pots desilenciar el compte des de la seva pàgina de perfil. + %1$s està silenciada fins a %2$s.\n Feu clic aquí per desactivar el silenciament. + + No hi ha notificacions per mostrar + t\'ha mencionat + wrote a new message + ha difós el teu missatge + ha marcat com a preferit el teu missatge + t\'ha començat a seguir + ha demanat de seguir-te + + i un altra notificació + i %d notificacions més + + + %d agradaments + %d agradaments + + Suprimir una notificació? + Suprimir totes les notificacions? + La notificació s\'ha suprimit! + S\'han suprimit totes les notificacions! + + Seguides + Seguidores + Fixats + + No s\'ha pogut obtenir la id del client! + No s\'ha pogut fer connexió al domini de la instància! + No hi ha connexió a internet! + El compte s\'ha blocat! + El compte ja no està blocat! + El compte s\'ha silenciat! + El compte ja no està silenciat! + Seguiment del compte confirmat! + Ja no estàs seguint el compte! + El bram s\'ha difós! + El bram ja no està difós! + S\'ha afegit el bram als teus preferits! + S\'ha eliminat el bram dels teus preferits! + S\'ha denunciat el bram! + S\'ha suprimit el bram! + S\'ha fixat el bram! + S\'ha desfixat el bram! + Ep! S\'ha produït un error! + Hi ha un error! La instància no ha generat cap codi d\'autorització! + El domini de la instància no sembla vàlid! + Hi ha hagut un error en passar d\'un compte a l\'altre! + Hi ha hagut un error durant la cerca! + S\'han guardat les dades del perfil! + No es pot endegar cap acció + El mèdia s\'ha guardat! + Hi ha hagut un error durant la traducció! + Tens l\'opció de traducció deshabilitada + S\'ha desat l\'esborrany! + Estàs segur que aquesta instància permet tants caràcters? El valor usual s\'aproxima a 500 caràcters. + S\'ha canviat la visibilitat dels brams d\'aquest compte %1$s + + Nombre de brams per càrrega + Sempre + Wi-Fi + Demana + Carrega els mèdia... + Carrega les imatges + Mostra\'n més... + Mostra\'n menys... + Contingut sensible + Desactiva els avatars GIF + Camí: + Desa els esborrany automàticament + Afegeix als brams els URL dels mèdia + Avisa quan algú et comenci a seguir + Avisa quan algú difongui el teu missatge + Avisa quan algú marqui el teu missatge com a preferit + Avisa quan algú et mencioni + Avisar quan una enquesta finalitzi + Notifica els brams nous + Demana confirmació abans de difondre + Demana confirmació abans d\'afegir a preferits + Enviar avisos només amb WIFI + Avisar? + Notificacions silencioses + Límit per mostrar material ofensiu (en segons, 0 és desactivat) + Temps d\'espera de la descripció dels mèdia (en segons, 0 vol dir desactivat) + Edita el perfil + Personalitza el compartir + Personalització del que comparteixo a URL… + Biografia... + Fes el compte privat + Desa els canvis + Escull una foto de capçalera + Ajusta la previsualització d\'imatges + A partir de quants caràcters es divideixen automàticament els brams de resposta: + Has assolit el màxim permès de 160 caràcters! + Has assolit el màxim permès de 30 caràcters! + Entre + i + Ha d\'haver passat més de %1$s + Ha d\'haver passat menys de %1$s + Hora d\'inici + Hora final + Utilitzar el navegador incorporat + Pestanyes personalitzades + Habilita el Javascript + Expandir automàticament el ac + Permetre galetes de terceres + Per a Yandex, pots deixar en blanc la clau de l\'API + + Fosc + Clar + Negre + + Color del LED: + + Blau + Cian + Magenta + Verd + Vermell + Groc + Blanc + + Seguir + Deixa de blocar + Silencia + Deixa de silenciar + S\'ha enviat la petició + Et segueix + Cerca + Primera lletra en majúscula a les respostes + Redimensiona les fotos + Redimensiona els vídeos + + Notificacions emergents + Sisplau, confirma les notificacions emergents que vols rebre. +Pots habilitar o deshabilitar les notificacions més endavant a la configuració (Pestanya de notificacions). + + Netejar la memòria cau + Hi ha %1$s de dades a la memòria cau.\n\nVoldries eliminar-les? + Mb + S\'ha netejat la memòria cau! S\'han alliberat %1$s + + Títol + Títol… + Descripció + Paraules clau + Paraules clau… + + Sincronitza + Filtra + Els teus brams + Les teves notificacions + Públic + No llistats + Privat + Directe + Algunes paraules clau… + Mostra els mèdia + Mostra els fixats + No s\'han trobat resultats! + Còpia de seguretat dels brams per %1$s + S\'han importat %1$s brams nous + %1$s s\'han importat noves notificacions + + Dates en ordre descendent + Dates en ordre ascendent + + + No + Només + Tots dos + + No s\'han trobat brams a la base de dades. Sisplau, pitgeu el botó de sincronitzar al menú per recobrar-los. + + Dades gravades + Només es guarda al dispositiu la informació bàsica dels comptes. +Aquestes dades són estrictament confidencials i només les pot usar l\'aplicació. +Quan s\'esborra l\'aplicació s\'eliminen les dades immediatament.\n +⚠ Els noms d\'usuària i contrasenyes no s\'emmagatzemen mai. Només s\'utilitzen durant una autentificació segura (SSL) en una instància. + Permisos: + - ESTAT_ACCES_XARXA: Usat per detectar si el dispositiu està connectat a una xarxa WIFI.\n + - INTERNET: Usat per a requeriments a una instància.\n + - ESCRIURE_MAGATZEM_EXTERN: Usat per emmagatzemar mèdia o moure l\'app a una tarja SD.\n + - LLEGIR_MAGATZEM_EXTERN: Usat per afegir mèdia als toots.\n + - BOOT_COMPLET: Usat per iniciar el servei de notificacions.\n + - DESPERTA_BLOCATGE: Usat durant el servei de notificacions. + Permisos d\'API: + - Lectura: Lectura de dades.\n + - Escriptura: Publica missatges i carrega mèdia per a missatges.\n + - Seguiments: segueix, desegueix, bloca, desbloca.\n\n + ⚠ Aquestes accions es realitzen només a petició de la usuària. + + Rastreig i llibreries + L\' aplicació no fa ús d\'eines de rastreig (mesures d\'audiència, informes d\'errors, etc.) i no conté publicitat.\n\n + es minimitza l\'ús de les biblioteques: \n + - Lliscar: Per gestionar els mèdia\n + - Tasques-Android: Per gestionar serveis\n + - FotoVista: Per gestionar imatges\n + + Traducció dels brams + L\'aplicació permet la traducció de brams a través de la configuració regional del dispositiu i l\'API Yandex.\n + Yandex té la seva pròpia política de privacitat, disponible aquí: https://yandex.ru/legal/confidential/?lang=en + + Agraïments: + + Filtratge mitjançant expressions regulars + Cerca + Suprimeix + Arreplega més brams… + + Llistes + Segur que vols suprimir permanentment la llista? + No hi ha encara res a la llista. Quan els membres de la llista pengin nous missatges, apareixeran aquí. + Afegeix a la llista + Afegeix una llista + Suprimeix la llista + Edita la llista + Títol de llista nou + S\'ha afegit el compte a la llista! + Encara no tens cap llista! + + %1$s s\'ha traslladat a %2$s + L\'autenticació falla? + Aquí hi ha algunes comprovacions potencialment útils:\n\n +- Comprova que no hi hagi errors de picatge en el nom de la instància\n\n - Comprova que no tinguis la instància inactiva\n\n - Si tens l\'autenticació de dos factors (2FA), prova l\'enllaç de la part inferior (després d\'entrar el nom de la instància)\n\n - L\'enllaç el pots fer servir encara que no tinguis la 2FA\n\n - Si encara no rutlla, sisplau obre fil del Framagit a https://framagit.org/tom79/fedilab/issues + S\'ha carregat el mèdia. Toca aquí per mostrar-lo. + Aquesta acció pot durar temps. Rebràs un avís quan estigui enllestida. + Segueix processant, espereu sisplau… + Exporta missatges + Exporta els missatges de %1$s + S\'han exportat %1$s brams d\'un total de %2$s. + Ha fallat alguna cosa durant l\'exportació de dades de %1$s + Ha fallat alguna cosa mentre s\'exportaven dades! + Ha fallat alguna cosa mentre s\'importaven dades! + + Servidor intermediari + Vols habilitar el servidor intermediari? + Servidor + Port + Iniciar Sessió + Contrasenya + Inclou detalls del bram en compartir-lo + Dona suport a l\'App al Liberapay + Hi ha un error en l\'expressió regular! + En aquesta instància no hi hem trobat pissarres! + Esborrar la instància? + Tradueix al + Segueix la instància + Ja estàs seguint aquesta instància! + Confirmat, ja segueixes la instància! + Sòcies + Informació + Amaga les difusions de %s + Incloure al perfil + Mostra difusions de %s + No incloure al perfil + El compte ara s\'inclou al perfil + El compte ja no s\'inclou al perfil + Les difusions estan visibles! + Les difusions estan amagades! + Missatges directes + Filtres + No hi ha filtres per mostrar. En podeu crear fent clic al botó \"+\". + Frase o paraula clau + Pissarra pròpia + Pissarres públiques + Notificacions + Converses + Es detectaran independentment de les majúscules dins el text o avís de contingut d\'un bram + Descarta enlloc d\'amagar + Els brams filtrats desapareixeran del tot, encara que després s\'elimini el filtre + Quan les frases o les paraules clau siguin alfanumèriques, només s\'aplicaran a les coincidències de mots sencers + Mot sencer + Contexts de filtre + El context o conjunt de contexts on ha d\'operar un filtre + Expira el + Eliminar el filtre? + Actualitza el filtre + Crear un filtre + A qui seguir + No hi ha comptes llistats de moment! + Segueix + Seleccionar-ho tot + Deseleccionar-ho tot + %s és seguit! + La llista s\'està creant %s + Afegint comptes a la llista + S\'han afegit els comptes a la llista + Afegint comptes a la llista + No has creat cap llista encara. Clica el botó \"+\" per afegir-n\'hi una. + A qui seguir + API troncal + No es pot seguir el(s) compte(s) + Arreplegant compte remot + Expandeix automàticament els mèdia amagats + Nou seguiment + Nova difusió + Nou preferit + Nova menció + Enquesta finalitzada + Bram nou + Còpia de seguretat de brams + Brams nous + Baixada de mèdia + Canvi del so d\'avís + Selecciona el so + Activa interval de temps + Tutorials en vídeo + Arreplegant el fil remot! + No hi ha dominis blocats! + Desbloca el domini + Segur que vols desblocar %s? + Segur que vols blocar %s?\n\nJa no veuràs cap contingut del domini en cap pissarra pública ni en les notificacions. S\'eliminaran els teus seguidors provinents del domini. + Dominis blocats + Bloca el domini + El domini està blocat + El domini ja no està blocat! + Arreplegant missatge remot + Comentari + Instància de Paertub + Estrena els comentaris a aquest vídeo amb el botó de dalt a la dreta! + %s visites + Durada: %s + Afegeix-hi una instància + Aquest vídeo no acull comentaris! + Tria una resolució + Preferits del Paertub + S\'ha afegit el vídeo als marcadors! + S\'ha eliminat el vídeo dels marcadors! + No tens vídeos de Paertub als preferits! + Canal + Vídeos + Canals + Fer ús d\' Emoji One + Informació + Mostra la vista preliminar de tots els brams + Nou dissenyador UX/UI + Mostrar les vistes preliminars dels vídeos + S\'ha copiat la id del compte al portapapers! + Canvi d\'idioma + Idioma per defecte + Truncar els brams llargs + Truncar els brams per sobre de les \'x\' línies. El zero vol dir inactiu. + Mostra\'n més + Mostra\'n menys + Gestionar etiquetes + L\'etiqueta ja existeix! + L\'etiqueta s\'ha guardat! + S\'ha canviat l\'etiqueta! + S\'ha eliminat l\'etiqueta! + Temporitza difusió + Aquesta difusió s\'ha temporitzat! + No hi ha temporitzacions de difusió per mostrar! + Temporitza la difusió.]]> + Pissarra d\'art + Obrir menú + Torna + Logo de l\'aplicació + Foto de perfil + Bànner de perfil + Contacta l\'administradora de la instància + Afegir nou + Logo del MastoServidor + Tria d\'emojis + Refresca + Mostra tota la conversa + Suprimeix el compte + Esborra el domini blocat + Personalització de la tria d\'emojis + Reproduir vídeo + Bram nou + Imatge de la targeta + Amaga els mèdia + Icona de preferits + Afegir descripció del mèdia (per a disminuïdes visuals) + + Mai + 30 minuts + 1 hora + 6 hores + 12 hores + 1 dia + 1 setmana + + En aquest camp, cal que hi escriguis el nom del servidor de la teva instància.\nPer exemple, si has creat el compte a https://mastodon.social\n Només cal escriure mastodon.social (sense https://)\n + Pots començar amb les primeres lletres i ja se suggeriran noms.\n\n + ⚠ El botó d\'inici de sessió només funcionarà si el nom d\'instància és vàlid i la instància està activa! + + Més informació + + Idiomes + Només mèdia + Mostra contingut sensible + Traduccions Crowdin + Gestor de Crowdin + Traducció de l\'aplicació + Sobre Crowdin + Bot + Instància Pixelfed + Instància de Mastodont + Qualsevol d\'aquestes + Totes aquestes + Cap d\'aquestes + Qualsevol d\'aquestes paraules (separades-per-espais) + Totes aquestes paraules (separades-per-espais) + Afegir paraules al filtre (separades per espais) + Canvi de nom de columna + Instància Misskey + No tens instal·lada cap app que gestioni aquest tipus d\'enllaç. + Subscripcions + Visió general + Tendències + Afegits recentment + Local + Pujar + Respondre + Esborrar un comentari + Estàs segur d\'esborrar aquest comentari? + Vídeo a pantalla completa + Mode per a vídeos + Seleccioneu el document per pujar + Els meus vídeos + Títol + Llicència + Categoria + Idioma + Aquest vídeo conté contingut explícit o per adults + Habilita els comentaris als vídeos + Actualitza el vídeo + Descripció + S\'ha actualitzat el vídeo! + S\'ha cancel·lat la pujada! + S\'ha pujat el vídeo! + S\'està pujant, espera sisplau… + Feu clic aquí per editar les dades de vídeo. + Eliminar vídeo + Segur que vols suprimir aquest vídeo? + Mostra vídeos de contingut sensible + No hi ha vídeos per mostrar! + Deixa un comentari + Comparteix + Tria un mode de temporització + Des del dispositiu + Des del servidor + Brams (servidor) + Brams (al meu dispositiu) + Modifica + Mostra nous brams per damunt del botó \"Arreplegar-ne més\" + Pissarres + Interfícies + Contactes + %1$s ha comentat el teu vídeo %2$s]]> + %1$s segueix el teu canal %2$s]]> + %1$s segueix el teu compte]]> + %1$s]]> + %1$s]]> + %1$s]]> + %1$s ha publicat un nou vídeo: %2$s]]> + %1$s en una llista negra]]> + %1$s d\'una llista negra]]> + Exportar dades + Importar dades + Selecciona el document a importar + S\'ha produït un error en seleccionar la còpia de seguretat! + Afegir un comentari públic + Enviar comentari + No hi ha connexió a Internet. El missatge s\'ha guardat entre els esborranys. + Text sense format + HTML + Assenyala + Sortir de la sessió + Tot + Fes un donatiu a l\'app + Open Collective facilita a grups de crear ràpidament un col·lectiu, recollir diners i gestionar-los amb transparència. + Copia l\'enllaç + Connectar + Normal + Compactar + Consola + Tria mode de presentació + Proveïdora de pedaços de seguretat + Actualitzar el rastreig de dominis + S\'ha actualitzat la base de dades de rastreig! + trucades http blocades per l\'aplicació + Llista de trucades blocades + Sol·licita + S\'ha exportat la base de dades! + Hashtags recomanats + Pissarra de filtres amb etiquetes + Sense etiquetes + Amaga el botó \"suprimir\" de la pestanya de notificacions + Adjunta una imatge en compartir un URL + + Enquesta + Enquestes + Crea una enquesta + Opció 1 + Opció 2 + Opció %d + Per a una enquesta, cal que hi hagi almenys dues opcions! + Enllestit + acaba a %s + Actualitza l\'enquesta + Vota + S\'ha acabat una enquesta en què havies participat + Ha finalitzat una enquesta on has votat + Personalitza + Categories + Franja horària + Avançat + Mostra el senyal \'nou\' en brams no llegits + Paertub + Mou la pissarra + Amaga la pissarra + Reordenar pissarres + Llista eliminada definitivament + La instància s\'ha deixat de seguir + L\'etiqueta ja no està fixada + Desfés + Cal que restin visibles dues pestanyes! + Reordenar pissarres + Les pissarres principals només es poden amagar! + Codi-BB + Marcar tots els mèdia com a material sensible + Instància de GNU + Estatus en memòria cau + Incloure etiquetes a les respostes + Pitjada llarga per emmagatzemar mèdia + Tornar borrosos els mèdia sensibles + Mostra pissarres com a llista + Mostra pissarres + Marcar els brams fets per bots + Gestionar etiquetes + Recordar la posició en la pissarra principal + Historial + Llistes de reproducció + Nom que es mostrarà + No tens llistes de reproducció. Clica la icona \"+\" per afegir-ne de noves + Cal que et mostris amb algun nom! + Aquest canal és obligat quan la llista és pública. + Crea una llista de reproducció + La llista encara és buida. + tornar-hi + Galeria + Emoticones + Enganxina + Esborrador + Text + Filtre + Pinzell + Esteu segur per sortir sense desar la imatge? + Descarta + Desant… + Imatge desada amb èxit! + No s\'ha pogut desar la imatge + Opacitat + Habilita l\'editor de fotos + Afegeix un ítem a l\'enquesta + Eliminar el darrer ítem de l\'enquesta + Silenciar conversa + Desilenciar conversa + La conversa ja no està silenciada! + Conversa silenciada + Mostra les característiques de l\'aplicació + Silenciament programat + Esmenta el compte + Refrescar la memòria cau + Esmenta el missatge + Notícies + General + Regional + Art + Periodisme + Activisme + Jocs + Tecnologia + Contingut per a adults + Peluts + Alimentació + Logo de la instància + Alguna cosa ha fallat en comprovar la disponibilitat d\'instàncies! + Uneix-te a Mastodont + Tria una instància mitjançant una categoria, i després clica un botó de comprovació. + Per triar instància, activa el quadret corresponent. + %1$s usuàries + Confirma contrasenya + Estic d\'acord amb %1$s i %2$s + normes del servidor + condicions del servei + Inscriu-te + Aquesta instància va per invitació. Caldrà que una administradora aprovi el teu compte manualment abans de poder-lo usar. + Cal que ompliu tots els camps! + Les contrasenyes no coincideixen! + El correu electrònic no sembla ser vàlid! + El teu nom d\'usuària serà únic a %1$s + Se t\'enviarà un correu de confirmació + Cal un mínim de 8 caràcters + La contrasenya hauria de tenir almenys 8 caràcters + El nom d\'usuària només pot contenir lletres, números i guions baixos + Compte creat! + Ja s\'ha creat el teu compte ja s\'ha!\n\n +Pensa de validar el correu electrònic dins les properes 48 hores.\n\n +Ara ja pots connectar-te al compte escrivint %1$s en el primer camp i fent clic a Connectar.\n\n Important: Si la teva instància requereix validació, rebràs un correu electrònic un cop validada! + Desar el missatge entre els esborranys? + Administració + Informes + No hi ha informes per mostrar! + Reconnectar el compte + L\'aplicació no ha pogut accedir a les característiques d\'administradora. Pot ser que calgui reconnectar amb el compte per tenir totes les funcions. + No s\'ha resolt + Remot + Actiu + Pendent + Deshabilitat + En silenci + Suspès + Permisos + Estat del correu-e + Estat de la sessió + Inscrit + IP més recent + Advertir + Deshabilita + Silenciar + Notificar usuària per correu-e + Advertiment personalitzat + Usuària + Moderadora + Administradora + Confirmat + No confirmat + Missatges denunciats + Compte d\'usuària + Desactivar silenciador + Desfer desactivar + Suspendre + Desfer suspendre + El compte està silenciat! + El compte ja no està silenciat! + Aquest compte està suspès! + El compte ja no està suspès! + El compte està desactivat! + El compte ja no està desactivat! + El compte té un advertiment! + Mostrar el menú d\'administradora + Mostra la característica d\'administradora en els missatges + Permet + El compte està aprovat! + El compte ha estat rebutjat! + Assigna-m\'ho a mi + Desassigna + Marca com a resolt + Marca com a no resolt + Buida el contingut! + Mostrar el botó de funcionalitats de Fedilab + A l\'aplicació li cal accedir a la gravadora d\'àudio + Missatge de veu + Activa resposta ràpida + El compte a qui respons pot ser que no pugui veure el teu missatge! + Quan està desactivada, l\'app carrega sempre els últims missatges + Quan està desactivat, els mèdia senzills resten amagats amb un botó + Guarda els mèdia a mida completa pitjant les previsualitzacions durant un segon + Afegeix un botó el·líptic a dalt a la dreta per llistar totes les etiquetes/instàncies/llistes + Dins el període definit, l\'app enviarà notificacions. Pots revertir (p. e. silenciar) aquest període amb l\'opció pertinent. + Mostrar un botó de Fedilab sota la imatge de perfil. És una drecera per accedir a funcions de l\'app. + Permet de respondre directament dins les pissarres sota les infos d\'estat + Les previsualitzacions de les pissarres no es retallaran + Permet reproduir vídeos incrustats dins les pissarres + Permet de revertir la forma de llegir els missatges que es mostren al clicar el botó de veure més + Aquesta opció permet donar suport als recursos de xifratge més recents. És últil per als dispositius Android més antics o per si no et pots connectar a la teva instància. + Exclusivament per a vídeos de Paertub. Canvia el mode en cas de què no els puguis reproduir. + Aquestes etiquetes permetran de filtrar els estatus dels perfils. Hauràs de fer servir el menú contextual per veure\'ls. + Inserir automàticament un salt de línia després de la menció per posar la primera lletra en majúscula + Permet als creadors de continguts de compartir missatges en els seus RSS + Redactar + Màxim d\'intents quan es penja un mèdia + Crear una carpeta nova aquí + Introdueix el nom de la carpeta + Cal introduir un nom de carpeta vàlid + Aquesta carpeta ja existeix.\n Cal assignar a la carpeta un altre nom + Selecciona + Directori Predeterminat + Carpeta + Crear carpeta + Vols veure missatges efímers després de completar accions (difusió, pref, etc.)? + Les instàncies silenciades s\'han exportat! + Afegir una instància + Exportació d\'instàncies + Importació d\'instàncies + Informes de fallida + Habilita els informes de fallida + Quan està habilitat, es crea un informe local de fallida que després pots compartir. + Malaslab s\'ha aturat :( + Pots enviar-me l\'informe de la fallada per email. Ajudarà a resoldre-ho :)\n\nPots afegir-hi contingut. Gràcies! + Utilitza el wysiwyg (es-veu-com-ho-veus) + Quan està habilitat, es pot formatar el text fàcilment amb unes eines. + Estadí­stiques + Missatges totals + Nombre de difusions + Nombre de preferits + Nombre de mencions + Nombre de seguiments + Nombre d\'enquestes + Nombre de respostes + Nombre de missatges + Missatges + Visibilitat + Nombre d\'ítems amb mèdia + Nombre d\'ítems amb mèdia sensibles + Nombre amb AC + Data del primer missatge + Data del darrer missatge + Data de la primera notificació + Data de l\'última notificació + Freqüència + %s missatges per dia + %s notificacions per dia + Interval de dates + Grups + No hi ha grups! + Deshabilita els emojis animats personalitzats + Gràfiques + Mostra les gràfiques + L\'aplicació està recollint les dades locals, cal esperar... + Còpia de seguretat + Missatges de la còpia de seguretat automàtica + Aquesta opció s\'aplica a un sol compte. Posarà en marxa un servei que guardarà localment els teus missatges de forma automàtica a la base de dades. Això permet obtenir estadístiques i gràfiques + Notificacions de còpia de seguretat automática + Opció lligada a cada compte. Activa un servei que guarda automàticament les notificacions a la base de dades local. Això permet obtenir estadístiques i gràfiques + Denuncia el compte + Envia una invitació + La teva instància no permet donar d\'alta un altre compte! + + %d vot + %d vots + + + %d votant + %d votants + + + Elecció única + Elecció múltiple + + + 5 minuts + 30 minuts + 1 hora + 6 hores + 1 dia + 3 dies + 7 dies + + + Visualització web + Stream directe + + Per unir-te a la meva instància\"%1$s\", pots descarregar Fedilab:\n\nF-Droid: %2$s\nGoogle: %3$s\n\nDesprés obres l\'enllaç de sota amb Fedilab i crees el teu compte :)\n\n%4$s + + Una enquesta no pot tenir opcions duplicades! + Per a tots els comptes + Memòria cau de la base de dades + Neteja la memòria cau de la pissarra personal + Neteja els missatges guardats a la cau + Neteja els marcadors + Arxius de la cau + Notificacions totals + Amaga els ítems del menú + Fedilab té actives les notificacions en directe + Per %1$s comptes amb %2$s esdeveniments + Notificacions en directe per %1$s + Es desactivaran les notificacions en directe només d\'aquest compte. + Esborra la memòria cau en sortir + La memòria cau (mèdia, missatges registrats, dades del navegador incorporat) s\'esborraran automàticament en sortir de l\'aplicació. + Vols deixar de seguir aquest compte? + Mostra diàleg de confirmació abans de deixar de seguir + Substituir Youtube amb Invidio.us + Invidious és una alternativa a YouTube en funcions de frontal + Escriu aquí el teu servidor particular o deixa-ho buit i s\'usarà invidio.us + Substitueix Twitter amb Nitter + Nitter és una alternativa de codi obert que fa de frontal per Twitter i protegeix la privacitat. + Introdueix aquí el teu servidor personalitzat o deixa-ho en blanc i s\'usarà nitter.net + Substituir Instagram per Bibliogram + Bibliogram és un frontal alternatiu de codi obert per Instagram que protegeix la privacitat. + Introdueix aquí el teu servidor personalitzat o deixa-ho en blanc i s\'usarà bibliogram.art + Substituïu Reddit per Libreddit + Libreddit és una interfície alternativa a Reddit, és de codi obert i prioritza la privadesa. + Introduïu el vostre servidor personalitzat o deixeu-ho em blanc per utilitzar libredd.it + Substitueix els enllaços de Medium + Substitueix els enllaços de medium.com per un frontal alternatiu de codi obert que prioritza la privacitat. + Per defecte: scribe.rip + Substitueix els enllaços de la Viquipèdia + Substitueix els enllaços a la Viquipèdia per un frontal alternatiu que prioritza les privacitat. + Per defecte: wikiless.org + Amaga la barra de notificacions de Fedilab + Per amagar la notificació que queda a la barra d\'estat, toca la icona de l\'ull i després desactiva: \"Mostra a la barra d\'estat\" + Activa els missatges d\'aplicació per rebre notificacions en temps real. + Notificacions en directe desactivades + Notificacions emergents + Les notificacions es recolliran cada 15 minuts. + Afegeix-hi notes + Notes per al compte + Permet la compressió de fotos grans en més petites amb una pèrdua mínima o negligible de qualitat d\'imatge. + Permet de comprimir els vídeos tot mantenint-ne la qualitat. + L\'app està comprimint els mèdia, no trigarà gaire… + Canviar la icona de l\'aplicació + Toca per canviar la icona de l\'aplicació + Publica + Visibilitat de la publicació + Toca aquí per afegir fotos + Formats acceptats: jpeg, png, gif \n\nMida Màxima: 15 MB \n\nCada àlbum pot incloure fins a 4 fotos o vídeos + Pujar mèdia + Afegeix llegenda opcional + L\'aplicació ha rebut un missatge d\'error molt llarg des de l\'API %1$s + Previsualització de missatge + Afegeix mencions en cada missatge + S\'està extraient la conversa + Ordenar per + Títol del vídeo + Unir-se al Paertub + Tinc almenys 16 anys i accepto les %1$s d\'aquesta instància + Enllaços + Canviar el color dels enllaços (Url, mencions, etiquetes, etc.) en els missatges + Capçalera de difusions + Canviar el color del nom que es mostra al capdamunt dels missatges + Canviar el color del nom d\'usuària al capdamunt dels missatges + Canviar el color de la capçalera en les difusions + Publicacions + Color de fons de les publicacions en les pissarres + Restablir el colors + Pitja aquí per restablir tot els colors personalitzats + Reiniciar + Icones + Color de les icones inferiors a les pissarres + Fixa l\'etiqueta + Logo de la instància + Edita el perfil + Endega un acció + Traducció + Previsualització d\'imatge + Color del text + Canvia el color del text en les publicacions + Aplicar els canvis + Cal que reiniciïs l\'aplicació per activar els canvis + Reinicia + Usa un tema personalitzat + Autoritza a canviar colors del tema seleccionat + Gestió de temes + Arxivar prèviament + S\'ha exportat el tema + S\'ha exportat el tema en format CSV + Assigna el color primari a la barra d\'estat + Color de la barra d\'estat + Recobra el tema de fàbrica + Importar un tema + Toca aquí per importar un tema prèviament exportat + Exporta el tema + Toca aquí per exportar el tema actual + Hi ha hagut un error en la selecció del document del tema + Selector de temes + Tria un tema pre-instal·lat + Temes + Aplica el color primari a la barra de navegació + Color de la barra de navegació + Color subjacent del contingut de l\'aplicació. + Color de fons + Ressalta cromàticament parts de la IU. + Color d\'èmfasi + Els mostrats amb més freqüència en la teva aplicació. + Color primari + Exporta els marcadors de la instància + Importa els marcadors de la instància + Nombre d\'usuàries + Nombre de missatges + Nombre d\'instàncies + Blocat + Finalitza en %s + Novetats de %s + Pots seguir el meu compte per mantenir-te informada + La instància no figura a https://instances.social + Mostra l\'enllaç sencer + Compartir enllaç + S\'ha copiat l\'URL al porta-retalls + Obre amb una altra aplicació + Comprova la redirecció + El redireccionament de la URL no funciona + %1$s \n\nredireccions a\n\n %2$s + Canvieu l\'agent d\'usuari + Set a custom user agent or leave blank + Allows to customize the user agent used for api calls or with the built-in browser. + Eliminar seguiments de màrketing (UTM) + L\'aplicació eliminarà automàticament els seguiments de màrketing (UTM) dels URL abans de seguir un enllaç. + Tendències + Tendències ara + %d persones parlant + Comptes de Twitter (via Nitter) + Usuaris de Twitter separats per espais + Proves d\'identitat + Verified identity + Verified by %1$s (%2$s) + Suprimir la notificació + Mostrar més opcions + És una història de Pixelfed + Puja un contingut i s\'afegirà automàticament a la teva història de Pixelfed. + Contingut afegit correctament a la teva història! + Acció deshabilitada + Deixa de seguir + Alguna cosa no ha rutllat, si us plau comprova el directori de descàrregues a la configuració. + Avisos + No hi ha avisos! + Afegir una reacció + Utilitza el teu navegador preferit dins l\'aplicació. Desmarca aquesta funció si vols que els enllaços s\'obrin externament. + Memòria cau de vídeo en MB, el zero vol dir que no hi ha memòria cau. + Marques d\'aigua + Afegir automàticament una marca d\'aigua a les imatges. El text és personalitzable per a cada compte. + No s\'han trobat distribuïdors! + Per rebre missatges d\'aplicació et cal un distribuïdor.\nPer més detalls vegeu %1$s.\n\nTambé pots desactivar els missatges a la configuració si els vols ignorar. + Tria un distribuïdor + diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml new file mode 100644 index 000000000..328fe1526 --- /dev/null +++ b/app/src/main/res/values-cs/strings.xml @@ -0,0 +1,1156 @@ + + + Otevřít nabídku + Zavřít nabídku + O aplikaci + O instanci + Soukromí + Cache + Odhlásit + Přihlásit + + Zavřít + Ano + Ne + Zrušit + Stáhnout + Stáhnout %1$s + Média uložena + Soubor: %1$s + Heslo + Email + Účty + Tooty + Štítky + Uložit + Obnovit + Žádné výsledky! + Instance + Instance: mastodon.social + Nyní používáte účet %1$s + Přidat účet + Obsah tootu byl zkopírován do schránky + Adresa tootu byla zkopírována do schránky + Změnit + Vybrat obrázek… + Vyčistit + Fotoaparát + Smazat všechno + Přeložit tento toot. + Naplánovat + Velikost textu a ikony + Změnit aktualní velikost textu: + Změnit velikost ikony: + Další + Předchozí + Otevřít čím + Ověřit + Média + Sdílet s + Sdílet prostřednictvím Fedilab + Odpovědi + Uživatelské jméno + Koncepty + Oblíbené + Noví sledující + Zmínky + Boosty + Zobrazit boosty + Zobrazit odpovědi + Otevřít v prohlížeči + Přeložit + Prosím počkejte několik sekund před provedením této akce. + + Domů + Místní časová osa + Federovaná časová osa + Nastavení + Oblíbené + Komunikace + Ztlumení uživatelé + Blokovaní uživatelé + Oznámení + Žádost o sledování + Nastavení + Smazat účet + Odstranit účet %1$s z aplikace? + Poslat e-mail + Klikněte na cestu pro změnu + Neúspěšné! + Naplánované tooty + Níže uvedené informace mohou popisovat uživatelský profil neúplně. + Vložit smajlík + Aplikace prozatím nenačetla uživatelské smajlíky. + Push notifications + Are you sure you want to logout? + Are you sure you want to logout @%1$s@%2$s? + + Žádné tooty k zobrazení + No stories to display + Stories + Boostnuto uživatelem %1$s + Přidat tento toot k oblíbeným? + Odstranit tento toot z oblíbených? + Boostnout tento toot? + Zrušit boost? + Připnout tento toot? + Odepnout tento toot? + Ztlumit + Blokovat + Nahlásit + Odstranit + Kopírovat + Sdílet + Zmínit + Časově omezené ztlumení + Smazat a přepsat + + Ztlumit tento účet? + Blokovat tento účet? + Nahlásit tento toot? + Blokovat tuto doménu? + Zrušit ztlumení tohoto účtu? + Unblock this account? + + + Oznámit + Tichý režim + + + Odstranit tento toot? + Smazat a přepsat tento toot? + + Záložky + Přidat do záložek + Odstranit záložku + Záložky jsou prázdné + Toot byl přidán do záložek! + Toot byl odstraněn ze záložek! + + %d s + %d m + %d h + %d d + + %d second + %d seconds + %d seconds + %d seconds + + + %d minute + %d minutes + %d minutes + %d minutes + + + %d hour + %d hours + %d hours + %d hours + + + %d day + %d days + %d days + %d days + + + Varování + Co se vám honí hlavou? + TOOTNOUT! + QUEETNOUT! + cw + Napsat toot + Odpovědět na toot + Napsat queet + Odpovědět na queet + Vybrat média + Nastala chyba při výběru média! + Smazat médium? + Váš toot je prázdný! + Viditelnost tootu + Defaultní viditelnost tootů: + Toot byl odeslán! + Odpovídáte na tento toot: + Citlivý obsah? + + Posílat na veřejné časové osy + Neposílat na veřejnou časovou osu + Posílat pouze sledujícím + Posílat pouze zmíněným uživatelům + + Žádné koncepty! + Vyberte toot + Vyberte účet + Vyberte účty + Odstranit koncept? + Klepněte na tlačítko pro zobrazení původního tootu + Popsat pro zrakově postižené + + Popis není dostupný! + + Release %1$s + Vývojář: + Licence: + GNU GPL V3 + Zdrojový kód: + Překládat tooty: + Prohledat instance: + Návrh ikon: + + Konverzace + + Žádný účet k zobrazení + Není požadavek ke sledování + Tooty \n %1$s + Sleduji \n %1$s + Sledující \n %1$s + Připnuto \n %d + Povolit + Odmítnout + + Žádné naplánované tooty k zobrazení! + Napište toot a zvolte Plánovat z horního menu. + Odstranit naplánovaný toot? + Média: %d + Toot byl naplánován! + Plánované datum musí být vyšší než aktuální hodina! + Spořič baterie je aktivován! Nemusí fungovat podle očekávání. + + Časový interval pro ztlumení musí být vyšší než jedna minuta. + %1$s byl ztlumen až do %2$s. \n Ztišení účtu můžete zrušit z jeho/její profilové stránky. + %1$s je ztlumen do %2$s. \n Klikněte zde pro zrušení ztišení. + + Žádné upozornění k zobrazení + vás zmínil/a + wrote a new message + boostnul/a váš toot + si oblíbil/a váš toot + vás sleduje + asked to follow you + + and další upozornění + and %d další upozornění + and %d dalších upozornění + and %d dalších oznámení + + + %d se líbí + %d se líbí + %d se líbí + %d se líbí + + Smazat oznámení? + Smazat všechna oznámení? + Oznámení bylo smazáno! + Všechna oznámení byla smazána! + + Sleduji + Sledující + Připnuto + + Nelze načíst klientské id! + Nelze se připojit k doméně instance! + Nejste připojeni k internetu! + Tento účet je zablokován! + Účet není nadále blokován! + Účet byl ztlumen! + Účet není nadále ztlumen! + Účet je sledován! + Účet není nadále sledován! + Toot byl boostnut! + Toot již není boostnut! + Toot byl přidán k oblíbeným! + Toot byl odstraněn z oblíbených! + Toot byl nahlášen! + Toot byl smazán! + Toot byl připnut! + Toot byl odepnut! + Oops! Došlo k chybě! + Došlo k chybě! Instance nevrátila autorizační kód! + Tato doména není platná! + Došlo k chybě při přepínání mezi účty! + Při vyhledávání došlo k chybě! + Profil byl uložen! + Nelze vykonat akci + Média byla uložena! + Při překladu došlo k chybě! + Překlady jsou vypnuty v nastavení + Koncept uložen! + Jste si jisti, že tato instance dovoluje tento počet znaků? Obvyklá hodnota je 500 znaků. + Viditelnost tootů byla změněna pro účet %1$s + + Počet tootů pro jedno nahrání + Vždy + WIFI + Zeptat se + Nahrát média + Nahrát obrázky + Ukázat víc… + Ukázat méně… + Citlivý obsah + Zakázat GIF avatary + Cesta: + Ukládat koncepty automaticky + Přidat URL médií v tootu + Oznámení v případě sledování + Oznámení v případě boostnutí vašeho tootu + Oznámení v případě oblíbení vašeho tootu + Oznámení v případě, že vás někdo zmíní + Oznámení po skončení ankety + Notify for new posts + Zobrazit potvrzení před boostnutí + Zobrazit potvrzení před oblíbením + Oznámení pouze na WIFI + Oznámení? + Tichá oznámení + NSFW prodleva (vteřiny, 0 znamená vypnuto) + Media Description timeout (seconds, 0 means off) + Upravit profil + Vlastní sdílení + Vaše vlastní sdílecí URL… + Bio… + Zamknout účet + Uložit změny + Vyberte obrázek do hlavičky + Přizpůsobit náhled obrázků + Automaticky rozdělit tooty nad 500 znaků do odpovědí + Bylo dosaženo 160ti znakového limitu! + Bylo dosaženo 30ti znakového limitu! + Mezi + a + Čas musí být vyšší než %1$s + Čas musí být nižší než %1$s + Počáteční čas + Koncový čas + Použít vestavěný prohlížeč + Uživatelské záložky + Povolit JavaScript + Automaticky ukázat cw + Povolit soubory cookie třetích stran + Váš API klíč, prázdné pro Yandex + + Tmavé + Světlé + Černá + + Barva LED: + + Modrá + Tyrkysová + Fialová + Zelená + Červená + Žlutá + Bílá + + Následovat + Odblokovat + Ztlumit + Zrušit ztlumení + Žádost odeslána + Sleduje vás + Hledat + První písmeno velké v odpovědích + Změnit velikost obrázků + Resize videos + + Push oznámení + Prosím potvrďte push oznámení, která chcete dostávat. + Toto nastavení můžete změnit později v Nastavení (záložka Oznámení). + + + Vyprázdnit cache + V cache je %1$s dat.\n\nChcete je odstranit? + Mb + Cache byla vyprázdněna! %1$s bylo uvolněno + + Název + Název… + Popis + Klíčová slova + Klíčová slova… + + Synchronizovat + Filtrovat + Vaše tooty + Your notifications + Veřejné + Skryté + Soukromé + Přímé + Některá klíčová slova… + Zobrazit média + Zobrazit připnuté + Nebyly nalezeny odpovídající výsledky! + Zálohovat tooty pro uživatele %1$s + %1$s nových tootů bylo importováno + %1$s new notifications have been imported + + Dates descending + Dates ascending + + + Ne + Pouze + Oba + + V databázi nebyly nalezeny žádné tooty. Prosím použijte synchronizační tlačítko v menu pro obnovení. + + Zaznamenaná data + V zařízení jsou uloženy pouze základní informace o účtech. +Tyto informace jsou přísně tajné a mohou být použity pouze aplikací. +Smazaní aplikace okamžitě odstraní tyto údaje.\n +Uživatelské jméno a heslo nejsou nikdy ukládány. Jsou použity pouze během bezpečného přihlášení (SSL) k instanci. + Oprávnění: + - ACCESS_NETWORK_STATE: Použito k detekci WiFi připojení.\n + - INTERNET: Použito ke komunikaci s instancí.\n + - WRITE_EXTERNAL_STORAGE: Použito k ukládání médií nebo k přesunu aplikace na SD kartu.\n + - READ_EXTERNAL_STORAGE: Použito pro přidání medií k tootům.\n + - BOOT_COMPLETED: Použito k aktivaci oznamovací služby.\n + - WAKE_LOCK: Použito během oznamovací služby. + + API oprávnění: + - Čtení: Čtení dat.\n + - Zápis: Zveřejňování příspěvků a nahrávání médií k příspěvkům.\n + - Sledování: Sledování, zrušení sledování, blokování, odblokování.\n\n + ⚠ Tyto akce jsou vykonány pouze na vyžádání uživatele. + + Sledování a Knihovny + Aplikace nepoužívá sledovací nástroje (měření návštěvnosti, hlášení chyb, atd.) a neobsahuje reklamy.\n\n + Použití knihoven je minimální: \n + - Glide: Správa médií\n + - Android-Job: Správa služeb\n + - PhotoView: Správa obrázků\n + + Překlad tootů + Aplikace umožňuje překlad tootů do jazyka zařízení s použitím služby Yandex API.\n + Yandex má svá vlastní pravidla pro soukromí uživatelských dat zde: https://yandex.ru/legal/confidential/?lang=en + + Poděkování: + Filtrování regulárními výrazy + Hledat + Odstranit + Načíst více tootů… + + Seznamy + Jsi si jist/a, že chceš trvale odstranit tento seznam? + Tento seznam je zatím prázdný. Jakmile členové seznamu zvěřejní nové tooty, objeví se zde. + Přidat do seznamu + Přidat seznam + Odstranit seznam + Upravit seznam + Nový název seznamu + The account was added to the list! + You don\'t have any lists yet! + + %1$s se přesunul do %2$s + Přihlášení nefunguje? + Zde jsou kontroly, které by vám mohly pomoct:\n\n + - Zkontrolujte, že ve jmménu instance není překlep\n\n + - Ověřte, že vaše instance běží\n\n + - V případě, že používáte dvoufaktorovou autentizaci (2FA), použijte prosím odkaz dole (jakmile je vyplněno jméno instance)\n\n + - Můžete také použít tento link bez použití 2FA\n\n + - Pokud problém přetrvává, vytvořte tiket na https://framagit.org/tom79/fedilab/issues + + Média byla nahrána. Klikněte pro zobrazení. + Tato akce může trvat dlouho. Po dokončení obdržíte oznámení. + Zpracovávám, prosím čekejte… + Exportovat tooty + Exportovat tooty pro uživatele %1$s + %1$s tootů z %2$s bylo exportováno. + Něco se pokazilo během exportu dat pro uživatele %1$s + Něco se pokazilo během exportovaní dat! + Něco se pokazilo během nahrávaní dat! + + Proxy + Povolit proxy? + Host + Port + Přihlašovací jméno + Heslo + Přidat podrobnosti tootu při sdílení + Podpořit aplikaci na Liberapay + Chyba v regulárním výrazu! + Časová osa nenalezena na této instanci! + Odstranit instanci? + Přeložit v + Sledovat instanci + Tuto instanci již sledujete! + Instance je sledována! + Partnerství + Informace + Skrýt boosty od %s + Zmínit v profilu + Zobrazit boosty od %s + Neuvádět v profilu + Účet je nyní zmíněn v profilu + Účet není nadále zmíněn v profilu + Boosty jsou nyní zobrazeny! + Boosty jsou nyní skryty! + Přímá zpráva + Filtry + Žádné filtry k zobrazení. Můžete vytvořit nový filtr klepnutím na tlačítko \"+\". + Klíčové slovo nebo fráze + Domovská časová osa + Veřejná časová osa + Oznámení + Konverzace + Velikost písmen ani varování o obsahu nebudou brána v potaz + Zahodit místo skrytí + Filtrované tooty zmizí nezvratně i v případě, že je filtr později odstraněn + V případě, že klíčové slovo nebo fráze je pouze alfanumerické, filtr se uplatní pouze pokud odpovídá celému slovu + Celé slovo + Kontext filtru + Jeden nebo několik kontextů pro aplikaci filtru + Vyprší po + Vymazat filtr? + Aktualizovat filtr + Vytvořit filtr + Návrhy sledování + Momentálně není k dispozici žádný účet! + Následovat + Vybrat vše + Zrušit výběr + %s je sledován! + Vytvářím seznam %s + Přidávám účty do seznamu + Účty byly přidány do seznamu + Přidávám účty do seznamu + Žádný seznam dosud nebyl vytvořen. Můžete vytvořit nový seznam klepnutím na tlačítko \"+\". + Návrhy sledování + Trunk API + Účet/účty nemohou být sledovány + Načítám vzdálený účet + Automaticky zobrazovat skrytá média + Nový sledující + Nový boost + Nové oblíbení + Nová zmínka + Anketa skončila + Nový toot + Záloha tootů + New posts + Stahování médií + Výchozí zvuk oznámení + Vybrat tón + Zapnout rozvrh oznámení + Videonávody + Načítám vzdálenou konverzaci! + Žádné blokované domény! + Odblokovat doménu + Určitě chcete odblokovat %s? + Určitě chcete zablokovat %s?\n\nJiž z této domény neuvidíte ve všech veřejných časových osách ani v oznámeních žádný obsah. Vaši sledující z této domény budou odstraněni. + Blokované domény + Zablokovat doménu + Doména je blokována + Doména není nadále blokována! + Načítám vzdálený toot + Komentovat + Peertube instance + Zanechte první komentáře pro toto video. Klepněte na tlačítko vpravo nahoře! + %s zhlédnutí + Doba trvání: %s + Přidat instanci + Pro toto vide nejsou povoleny komentáře! + Vyberte rozlišení + Oblíbené na PeerTube + Video bylo přidáno do záložek! + Video bylo odstraněno ze záložek! + V oblíbených nemíte žádná videa na PeerTube! + Kanál + Videa + Kanály + Použít Emoji One + Informace + Zobrazit náhled ve všech tootech + Nový UX/UI návrhář + Zobrazit náhled videa + Identifikátor účtu byl zkopírován do schránky! + Změna jazyka + Základní jazyk + Ořezat dlouhé tooty + Ořezat tooty delší než \'x\' řádků. 0 znamená vypnuto. + Zobrazit více + Zobrazit méně + Spravovat štítky + Štítek již existuje! + Štítek byl uložen! + Štítek byl změněn! + Štítek byl smazán! + Naplánovat boost + Boost je naplánováo! + Žádný naplánovaý boost k zobrazení! + Naplánovat boost.]]> + Art časová osa + Otevřete nabídku + Zpět + Logo aplikace + Profilový obrázek + Profilová hlavička + Kontaktovat administrátora instance + Přidat nový + MastoHost logo + Výběr emotikonů + Načíst znovu + Ukázat celou konverzaci + Odebrat účet + Smazat blokovanou doménu + Uživatelský výběr emoji + Přehrát video + Nový toot + Obrázek karty + Skrýt média + Favicon + Médium pro přidání popisu + + Nikdy + 30 minut + 1 hodina + 6 hodin + 12 hodin + 1 den + 1 týden + + V tomto políčku vyplňte jméno vaší instance.\nPokud jste například vytvořili váš účet na https://mastodon.social\nvyplňte pouze mastodon.social (bez https://)\n + Začněte psát první znaky pro návrh.\n\n + ⚠ Tlačítko přihlášení bude fungovat pouze v případě, že je jméno instance platné a instance běží! + + Více informací + + Jazyky + Pouze média + Zobrazit NSFW + Překlady Crowdin + Crowdin manažer + Překlad aplikace + O Crowdin + Bot + Pixelfed instance + Instance Mastodon + Kterýkoliv + Všechny + Žádný + Kterékoliv slovo (odělené mezerami) + Všechna slova (oddělená mezerami) + Add some words to filter (space-separated) + Změnit název sloupce + Misskey instance + Na vašem zařízení není nainstalována žádná aplikace podporující tento odkaz. + Předplatné + Přehled + Populární + Naposledy přidané + Místní + Nahrát + Odpověděť + Odstranit komentář + Jste si jisti, že chcete smazat tento komentář? + Video přes celou obrazovku + Režim pro videa + Vybrat soubor pro nahrání + Moje videa + Název + Licence + Kategorie + Jazyk + Toto video je určeno pro dospělé nebo obsahuje explicitní obsah + Povolit komentáře k videu + Aktualizace videa + Popis + Video bylo aktualizováno! + Nahrávání zrušeno! + Video bylo nahráno! + Nahrávám, prosím čekejte… + Klepněte zde pro editaci video dat. + Odstranit video + Jste si jistý/á, že chcete smazat toto video? + Zobrazit NSFW (18+) videa + Žádné videa k zobrazení! + Zanechat komentář + Sdílet + Zvolte plánovací režim + Ze zařízení + Ze serveru + Tooty (Server) + Tooty (zařízení) + Upravit + Zobrazit nové tooty nad tlačítkem „Načíst více“ + Časové osi + Rozhraní + Kontakty + %1$s okomentoval/a vaše video %2$s]]> + %1$s sleduje váš kanál %2$s]]> + %1$s sleduje váš účet]]> + %1$s bylo publikováno]]> + %1$s uspěl]]> + %1$s selhal]]> + %1$s publikoval/a nové video: %2$s]]> + %1$s bylo přidáno na černou listinu]]> + %1$s bylo odstraněno z černé listiny]]> + Exportovat data + Importovat data + Vyberte soubor, který chcete importovat + Při výběru zálohového souboru nastala chyba! + Přidat veřejný komentář + Odeslat komentář + Nelze se připojit k internetu. Vaše zpráva byla uložena do konceptů. + Prostý text + HTML + Markdown + Odhlásit účet + Vše + Podpoř aplikaci + Open Collective umožňuje skupinám rychle založit kolektiv, získávat finanční prostředky a spravovat je transparentně. + Kopírovat odkaz + Připojit k + Normální + Kompaktní + Konzola + Nastavit režim zobrazení + Spravit poskytovatele bezpečnosti + Aktualizovat sledování domén + Základ sledovaných dat byl aktualizován! + volání http je blokováné aplikací + Seznam blokovaných domén + Odeslat + Databáze byla exportována! + Zvýrazněné hashtagy + Filtrovat časovou osu s hashtagy + Žádné hashtagy + Skrýt tlačítko pro vymazání oznámení ze záložky oznámení + Připojit při sdílení URL obrázek + + Anketa + Ankety + Vytvořit anketu + Volba 1 + Volba 2 + Volba %d + Pro anketu potřebujete alespoň dvě volby! + Hotovo + skončit po %s + Obnovit anketu + Hlasovat + Anketa, ve které jste hlasoval/a, skončila + Vaše anketa skončila + Přizpůsobit + Kategorie + Časový slot + Pokročilé + Zobrazit \"new\" na nových tootech + PeerTube + Přesunout časovou osu + Skrýt časovou osu + Změnit pořadí časových os + Seznam trvale smazán + Sledovaná instance odstraněna + Připnuté značky odstraněny + Vrátit zpět + Musíte si nechat dvě viditelné karty! + Změnit pořadí časových os + Hlavní časové linie mohou být pouze skryty! + BBCode + Vždy označovat média jako citlivá + GNU instance + Cached status + Forward tags in replies + Long press to store media + Rozostřít citlivá média + Display timelines in a list + Zobraz časové osy + Mark bot accounts in toots + Spravovat štítky + Remember the position in Home timeline + Historie + Playlisty + Zobrazované jméno + You don\'t have any playlists. Tap on the \"+\" icon to add a new playlist + Musíte zadat zobrazované jméno! + The channel is required when the playlist is public. + Vytvořit playlist + Na tomto playlistu není zatím nic. + provést znovu + Galerie + Emoji + Nálepka + Guma + Text + Filtr + Štětec + Jste si jisti, že chcete skončit bez uložení obrázku? + Zahodit + Ukladáno… + Image Saved Successfully! + Nepodařilo se uložit obrázek + Průhlednost + Povolit editor fotografií + Přidat položku ankety + Odstranit poslední možnost z ankety + Ztišit konverzaci + Unmute conversation + The conversation is no longer muted! + The conversation is muted + Otevřít vlastnosti aplikace + Časované ztlumení + Mention the account + Obnovit cache + Zmínit status + Novinky + Základní + Regionální + Umění + Žurnalistika + Aktivismus + Hrání + Technologie + Obsah pro dospělé + Furry + Jídlo + Logo of the instance + Something went wrong when checking available instances! + Připojte se k Mastodonu + Choose an instance by picking up a category, then tap on a check button. + Choose an instance by tapping on a check button. + %1$s users + Confirm password + I agree to %1$s and %2$s + pravidla serveru + podmínky užití + Registrovat se + This instance works with invitations. Your account will need to be manually approved by an administrator before being usable. + Please, fill all the fields! + Passwords don\'t match! + The email doesn\'t seem to be valid! + Your username will be unique on %1$s + You will be sent a confirmation e-mail + Použijte minimálně 8 znaků + Heslo musí mít minimálně 8 znaků + Username should only contain letters, numbers and underscores + Účet vytvořen! + Your account has been created!\n\n + Think to validate your email within the 48 next hours.\n\n + You can now connect your account by writing %1$s in the first field and click on Connect.\n\n + Important: If your instance required validation, you will receive an email once it is validated! + + Save the message in drafts? + Administrace + Reports + No reports to display! + Reconnect the account + The application failed to access the admin features. You might need to reconnect the account for having the correct scope. + Unresolved + Remote + Active + Pending + Disabled + Silenced + Suspended + Permissions + Email status + Login status + Joined + Most recent IP + Warn + Disable + Silence + Notify the user per e-mail + Custom warning + User + Moderator + Administrator + Confirmed + Not confirmed + Reported statuses + Account + Undo silence + Undo disable + Suspend + Undo suspend + The account is silenced! + The account is no longer silenced! + The account is suspended! + The account is no longer suspended! + The account is disabled! + The account is no longer disabled! + The account has been warned! + Display the admin menu + Display the admin feature in statuses + Allow + The account is approved! + The account is rejected! + Assign to me + Unassign + Mark as resolved + Mark as unresolved + Empty content! + Display Fedilab features button + The application needs to access audio recording + Voice message + Enable quick reply + The account you are replying might not see your message! + If disabled, the app will always load last statuses + If disabled, sensitive media will be hidden with a button + Store media in full size with a long press on previews + Add an ellipse button at the top right for listing all tags/instances/lists + During the time slot, the app will send notifications. You can reverse (ie: silent) this time slot with the right spinner. + Display a Fedilab button below profile picture. It is a shortcut for accessing in-app features. + Allow to reply directly in timelines below statuses + Previews will not be cropped in timelines + Allow to play embedded videos directly in timelines + Allow to reverse the way to read statuses that are displayed once tapping the fetch more button + This option allows to support recent cipher suites. It is useful for older Android devices or if you cannot connect to your instance. + Exclusively for Peertube videos. Switch this mode if you cannot play them. + These tags will allow to filter statuses from profiles. You will have to use the context menu for seeing them. + Automatically insert a line break after the mention to capitalize the first letter + Allow content creators to share statuses to their RSS feeds + Compose + Maximum retry times when uploading media + Create a new Folder here + Enter the folder name + Please enter a valid folder name + This folder already exists.\n Please provide another name for the folder + Select + Default Directory + Folder + Create folder + Display a toast message after an action has been completed (boost, fav, etc.)? + Muted instances have been exported! + Add an instance + Export instances + Import instances + Crash reports + Enable crash reports + If enabled, a crash report will be created locally and then you will be able to share it. + Fedilab přestal fungovat :( + Pošlete mi mailem údaje o chybě. Pomůžete tak při opravě :)\n\nMůžete přidat dodatečný obsah. Děkuji! + Use the wysiwyg + When enabled, you will be able to format your text easily with tools. + Statistics + Total statuses + Number of boosts + Number of favourites + Number of mentions + Number of follows + Number of polls + Number of replies + Number of statuses + Statuses + Visibility + Number with media + Number with sensitive media + Number with CW + First status date + Last status date + First notification date + Last notification date + Frequency + %s statuses per day + %s notifications per day + Date range + Groups + No groups! + Disable custom animated emojis + Charts + Display charts + The application collects your local data, please wait… + Backup + Auto backup statuses + This option is per account. It will launch a service that will automatically store your statuses locally in the database. That allows to get statistics and charts + Auto backup notifications + This option is per account. It will launch a service that will automatically store your notifications locally in the database. That allows to get statistics and charts + Report account + Send an invitation + Your instance does not allow to register a new account! + + %d hlas + %d hlasy + %d hlasu + %d hlasů + + + %d voter + %d voters + %d voters + %d voters + + + Jediná volba + Více voleb + + + 5 minut + 30 minut + 1 hodina + 6 hodin + 1 den + 3 dny + 7 dní + + + Webové zobrazení + Přímé streamovaní + + For joining my instance \"%1$s\", you can download Fedilab:\n\nF-Droid: %2$s\nGoogle: %3$s\n\nThen open the link below with Fedilab and create your account :)\n\n%4$s + + Your poll can\'t have duplicated options! + For all accounts + Database cache + Clear your home timeline cache + Clear your cached statuses + Clear your bookmarks + Files in cache + Total notifications + Hide menu items + Fedilab is running live notifications + For %1$s accounts with %2$s events + Live notifications for %1$s + Live notifications will be enabled for this account. + Clear cache when leaving + The cache (media, cached messages, data from the built-in browser) will be automatically cleared when leaving the application. + Do you want to unfollow this account? + Show confirmation dialog before unfollowing + Replace Youtube with Invidio.us + Invidious is an alternative front-end to YouTube + Enter your custom host or leave blank for using invidious.snopyta.org + Replace Twitter with Nitter + Nitter is an open source alternative Twitter front-end focused on privacy. + Enter your custom host or leave blank for using nitter.net + Replace Instagram with Bibliogram + Bibliogram is an open source alternative Instagram front-end focused on privacy. + Enter your custom host or leave blank for using bibliogram.art + Replace Reddit with Libreddit + Libreddit is an open source alternative Reddit front-end focused on privacy. + Enter your custom host or leave blank for using libredd.it + Replace Medium links + Replace medium.com links with an open source alternative front-end focused on privacy. + Default: scribe.rip + Replace Wikipedia links + Replace Wikipedia link with an open source alternative front-end focused on privacy. + Default: wikiless.org + Hide Fedilab notification bar + For hiding the remaining notification in the status bar, tap on the eye icon button then uncheck: \"Display in status bar\" + Use a push notifications system for getting notifications in real time. + No live notifications + Live notifications + Notifications will be fetched every 15 minutes. + Add notes + Notes for the account + Allow to compress large photos into smaller sized photos with very less or negligible loss in quality of the image. + Allow compressing videos while maintaining their quality. + The app is compressing the media, it can be quite long… + Change app icon + Tap to change the app icon + Post + Visibility of the post + Tap here to add photos + Accepted Formats: jpeg, png, gif \n\nMax File Size: 15 MB \n\nAlbums can contain up to 4 photos or videos + Upload media + Add an optional caption + The app received a very long error message from the API %1$s + Message preview + Add mentions in each message + Fetching conversation + Order by + Title for the video + Join Peertube + I am at least 16 years old and agree to the %1$s of this instance + Links + Change the color of links (URLs, mentions, tags, etc.) in messages + Reblogs header + Change the color of display name at the top of messages + Change the color of the user name at the top of messages + Change the color of the header for reblogs + Posts + Background color of posts in timelines + Reset colors + Tap here to reset all your custom colors + Reset + Icons + Color of bottom icons in timelines + Pin this tag + Logo of the instance + Edit profile + Make an action + Translation + Image preview + Text color + Change the text color in pots + Apply changes + You need to restart the application to apply changes + Restart + Use a custom theme + Allow to override colors of the selected theme above + Theming + Store before + The theme was exported + The theme has been successfully exported in CSV + Apply the primary color to the status bar + Status bar color + Restore a default theme + Import a theme + Tap here to import a theme from a previous export + Export the theme + Tap here to export the current theme + An error occurred when selecting the theme file + Theme Picker + Select a pre-installed theme + Themes + Apply the primary color to the navigation bar + Navigation bar color + The underlying color of the app’s content. + Background color + Accents select parts of the UI. + Accent color + Displayed most frequently across your app. + Primary color + Export bookmarks to the instance + Import bookmarks from the instance + User count + Status count + Instance count + Blocked + End in %s + What\'s new in %s + You can follow my account for updates + This instance is not available on https://instances.social + Display full link + Share link + The URL has been copied to the clipboard + Open with another app + Check redirect + This URL does not redirect + %1$s \n\nredirects to\n\n %2$s + Change the user agent + Set a custom user agent or leave blank + Allows to customize the user agent used for api calls or with the built-in browser. + Remove UTM parameters + The app will automatically remove UTM parameters from URLs before visiting a link. + Trends + Trending now + %d people talking + Twitter accounts (via Nitter) + Twitter usernames space separated + Identity proofs + Verified identity + Verified by %1$s (%2$s) + Delete the notification + Display more options + It is a Pixelfed story + Upload a media, it will be automatically added to your Pixelfed story. + Media successfully added to your story! + Action disabled + Unfollow + Something went wrong, please check your download directory in settings. + Announcements + No announcements! + Add a reaction + Use your favourite browser inside the app. Uncheck this feature to open links externally. + Video cache in MB, zero means no cache. + Watermarks + Automatically add a watermark at the bottom of pictures. The text can be customized for each account. + No distributors found! + You need a distributor for receiving push notifications.\nYou will find more details at %1$s.\n\nYou can also disable push notifications in settings for ignoring that message. + Select a distributor + diff --git a/app/src/main/res/values-cy/strings.xml b/app/src/main/res/values-cy/strings.xml new file mode 100644 index 000000000..c348bc73c --- /dev/null +++ b/app/src/main/res/values-cy/strings.xml @@ -0,0 +1,1173 @@ + + + Agor y ddewislen + Cau y ddewislen + Amdano + Am yr achos hwn + Preifatrwydd + Storfa + Allgofnodi + Mewngofnodi + + Cau + Ie + Na + Canslo + Lawrlwytho + Lawrlwytho %1$s + Arbedwyd y deunydd + Ffeil: %1$s + Cyfrinair + E-bost + Cyfrifon + Tŵtiau + Tagiau + Cadw + Adfer + Dim canlyniadau! + Achos + Achos: mastodon.social + Nawr yn gweithio gyda\'r cyfrif %1$s + Ychwanegu cyfrif + Mae cynnwys y tŵt wedi ei gopio i\'r clipfwrdd + The URL of the toot has been copied to the clipboard + Newid + Dewis llun… + Glân + Camera + Dileu i gyd + Cyfieithu\'r tŵt hwn. + Amserlen + Meintiau testun ac eiconau + Newid maint presennol y testun: + Newid maint presennol yr eicon: + Nesaf + Cynt + Agor gyda + Dilysu + Cyfryngau + Rhannu gyda + Rhannwyd drwy Fedilab + Ymatebion + Enwdefnyddiwr + Drafftiau + Ffefrynnau + Dilynwyr newydd + Crybwylliadau + Hybiadau + Dangos hybiadau + Dangos atebion + Agor mewn porwr + Cyfieithu + Arhoswch rhai eiliadau cyn gweithredu\'r cam hwn os gwelwch yn dda. + + Hafan + Llinell amser lleol + Llinell amser ffederasiwn + Opsiynau + Ffefrynnau + Cyfathrebu + Defnyddwyr mud + Defnyddwyr wedi’u blocio + Hysbysiadau + Dilyn ceisiadau + Gosodiadau + Dileu y cyfrif + Dileu y cyfrif %1$s o\'r rhaglen? + Anfon e-bost + Cliciwch ar y llwybr i\'w newid + Methwyd! + Tŵt Newydd + Gall y wybodaeth isod roi adlewyrchiad anghyflawn o broffil y defnyddiwr. + Mewnosod emoji + Ni gasglwyd emoji dethol gan yr ap am y tro. + Push notifications + Are you sure you want to logout? + Are you sure you want to logout @%1$s@%2$s? + + Dim tŵt i\'w arddangos + No stories to display + Stories + Hybwyd gan %1$s + Ychwanegu\'r tŵt hwn i\'ch ffefrynnau? + Cael gwared o\'r tŵt hwn o\'ch ffefrynnau? + Hybu\'r tŵt hwn? + Dad-hybu\'r tŵt hwn? + Pinio\'r tŵt hwn? + Dadbinio\'r tŵt hwn? + Mudo + Blocio + Cwyno + Dileu + Copïo + Rhannu + Crybwyll + Tawelu am gyfnod + Delete & re-draft + + Tawelu\'r cyfrif hwn? + Blocio\'r cyfrif hwn? + Cwyno am y tŵt hwn? + Blocio\'r parth hwn? + Unmute this account? + Unblock this account? + + + Hysbysu + Distaw + + + Cael gwared ar y tŵt hwn? + Dileu & ail-ddrafftio\'r tŵt hwn? + + Llyfrnodau + Ychwanegu at llyfrnodau + Cael gwared o\'r llyfrnod + Dim llyfrnodau i\'w harddangos + Mae\'r statws wedi ei ychwanegu i\'r llyfrnodau! + Cafwyd wared o\'r statws o\'r llyfrnodau! + + %d s + %d m + %d h + %d d + + %d seconds + %d second + %d seconds + %d seconds + %d seconds + %d seconds + + + %d minutes + %d minute + %d minutes + %d minutes + %d minutes + %d minutes + + + %d hours + %d hour + %d hours + %d hours + %d hours + %d hours + + + %d days + %d day + %d days + %d days + %d days + %d days + + + Rhybudd + Beth sydd ar eich meddwl? + Tŵt! + Cwît! + cs + Cyfansoddi tŵt + Ymateb i dŵt + Ysgrifennu cwît + Ymateb i gwît + Dewis cyfryngau + Roedd gwall! + Dileu\'r cyfryngau hyn? + Mae eich tŵt yn wag! + Gwelededd y tŵt + Gwelededd y tŵt fel y mae hi: + Mae\'r tŵt wedi\'i anfon! + Yr ydych yn ymateb i\'r tŵt hwn: + Cynnwys sensitif? + + Cyhoeddi ar ffrydiau cyhoeddus + Peidio a chyhoeddi ar ffrydiau cyhoeddus + Cyhoeddi i ddilynwyr yn unig + Cyhoeddi i ddefnyddwyr sy\'n cael eu crybwyll yn unig + + Dim drafftiau! + Dewiswch dŵt + Dewiswch gyfrif + Dewiswch ambell gyfrif + Dileu\'r drafft? + Cliciwch ar y botwm i ddangos y tŵt gwreiddiol + Disgrifio i\'r rheini â nam ar eu golwg + + Dim disgrifiad ar gael! + + Fersiwn %1$s + Datblygwr: + Trwydded: + GNU GPL V3 + Cod ffynhonnell: + Cyfieithiadau\'r tŵtiau: + Chwilio achosion: + Dylunydd eicon: + + Sgwrs + + Dim cyfrif i\'w arddangos + Dim cais dilyn + Tŵtiau \n %1$s + Yn dilyn \n %1$s + Dilynwyr \n %1$s + Wedi eu pinio \n %d + Caniatau + Gwrthod + + Dim tŵt wedi eu hamserlennu i\'w dangos! + Ysgrifenwch dŵt ac yna dewiswch Amserlen o\'r ddewislen uchaf. + Dileu\'r tŵt a amserlennwyd? + Cyfryngau: %d + Mae\'r tŵt wedi ei amserlennu! + Rhaid i ddyddiad yr amserlen fod yn hwyrach na\'r presennol! + Arbedwr batri wedi ei osod. Mae\'n bosib na fyddai\'n gweithio fel y disgwylir iddo wneud. + + Dylai\'r amser tawelu fod yn hirach na munud. + Mae %1$s wedi ei tawelu nes %2$s.\n Gallwch ddad-dawelu\'r cyfrif hwn o\'i tudalen proffil. + Mae %1$s wedi ei tawelu nes %2$s.\n Cliciwch yma i ddad-dawelu\'r cyfrif. + + Dim hysbysiad i\'w arddangos + wedi\'ch crybwyll + wrote a new message + wedi hybu\'ch tŵt + wedi nodi\'ch tŵt yn ffefryn + wedi\'ch dilyn chi + asked to follow you + + ac hysbysiad arall + ac %d hysbysiad arall + ac %d hysbysiad arall + and %d other notifications + and %d other notifications + and %d other notifications + + + %d wedi hoffi + %d like + %d likes + %d likes + %d likes + %d likes + + Dileu hysbysiad? + Dileu pob hysbysiad? + Mae\'r hysbysiad wedi ei ddileu! + Mae pob hysbysiad wedi ei ddileu! + + Yn dilyn + Dilynwyr + Wedi eu pinio + + Methwyd i gael gafael ar id y cleient! + Unable to connect to instance domain! + Dim cyswllt i\'r rhyngrwyd! + Blociwyd y cyfrif! + Nid yw\'r cyfrif wedi ei flocio mwyach! + Tawelwyd y cyfrif! + Nid yw\'r cyfrif wedi ei dawelu mwyach! + Dilynwyd y cyfrif! + Nid yw\'r cyfrif yn cael ei ddilyn mwyach! + Mae\'r tŵt wedi\'i hybu! + Nid yw\'r tŵt wedi\'i hybu mwyach! + Ychwanegwyd y tŵt i\'ch ffefrynnau! + Dilewyd y tŵt o\'ch ffefrynnau! + Cwynwyd am y tŵt! + Dilewyd y tŵt! + Piniwyd y tŵt! + Dad-biniwyd y tŵt! + Wps! Aeth rhywbeth o\'i le! + Aeth rhywbeth o\'i le! Ni wnaeth yr achos ddychwelyd cod awdurdodi! + Ymddengys nad yw parth yr achos yn un dilys! + Aeth rhywbeth o\'i le tra\'n symud rhwng cyfrifoedd! + Aeth rhywbeth o\'i le tra\'n chwilio! + Arbedwyd data y proffil! + Ni ellir gwneud dim + Arbedwyd y cyfryngau! + Aeth rhywbeth o\'i le tra\'n cyfieithu! + Translations are disabled in settings + Arbedwyd y drafft! + Ydych chi\'n siwr fyd yr achos hwn yn caniatau y nifer hyn o nodau? Fel arfer, tua 500 o nodau yw\'r nifer a ganiateir. + Gwelededd y tŵtiau wedi ei newid ar gyfer y cyfrif %1$s + + Nifer y tŵtiau i bob llwythiad + Bob tro + WIFI + Gofyn + Llwytho\'r cyfryngau + Llwytho\'r lluniau + Dangos mwy… + Dangos llai… + Cynnwys sensitif + Diffodd avatars GIF + Llwybr: + Arbed drafft yn ddiofyn + Ychwanegu URL o\'r cyfryngau yn y tŵtiau + Hysbysu pryd mae rhywun yn eich dilyn chi + Hysbysu pan mae rhywun yn hybu\'ch statws + Hysbysu pan mae rhywun yn ffefrynnu eich statws + Hysbysu pan mae rhywun yn sôn amdanoch chi + Notify when a poll ended + Notify for new posts + Dangos deialog cadarnhau cyn hybu + Dangos deialog cadarnhau cyn ychwanegu i\'r ffefrynnau + Ond hysbysu pan yn gysylltiedig i WIFI + Hsybysu? + Hysbysiadau distaw + Terfyn amser golwg deunydd anaddas i\'r gweithle (eiliadau, ystyr 0 yw ei fod wedi ei ddiffodd) + Media Description timeout (seconds, 0 means off) + Golygu proffil + Rhannu dethol + Eich URL… rhannu dethol + Bywgraffiad… + Cloi\'r cyfrif + Arbed newidiadau + Dewis llun pennawd + Ffitio delweddau rhagweld + Hollti tŵtiau dros 500 nodyn yn ymatebion + Yr ydych wedi llewnwi\'r 160 nodyn a ganiateir! + Yr ydych wedi llenwi y 30 nodyn a ganiateir! + Rhwng + ac + Rhaid i\'r amser fod yn fwy na %1$s + Rhad i\'r amser fod yn is na %1$s + Amser dechrau + Amser gorffen + Defnyddio\'r porwr rhagosodedig + Tabiau wedi\'u golygu + Caniatau Javascript + Ehangu cw yn awtomataidd + Caniatau cwcis trydydd parti + Eich allwedd API, mae modd ei adael yn wag i Yandex + + Tywyll + Golau + Du + + Gosod lliw LED: + + Glas + Cyan + Magenta + Gwyrdd + Coch + Melyn + Gwyn + + Dilyn + Dadflocio + Mudo + Dad-dawelu + Cais wedi ei anfon + Yn eich dilyn chi + Chwilio + Llythyren gyntaf yn llythyren fawr mewn ymatebion + Newid maint lluniau + Resize videos + + Hysbysiadau push + Cadarnhewch yr hysbysiadau push yr ydych am eu derbyn. + Mae modd galluogi neu diffodd yr hysbysiadau hyn wedyn yn gosodiadau (Tab hysbysiadau). + + + Clirio\'r storfa + Mae %1$s o ddata yn y storfa.\n\nHoffech chi ei ddileu? + Mb + Cliriwyd y storfa! Rhyddhawyd %1$s + + Teitl + Teitl… + Disgrifiad + Allweddeiriau + Allweddeiriau… + + Cydamseru + Hidlo + Eich tŵtiau + Your notifications + Cyhoeddus + Heb ei restru + Preifat + Preifat + Rhai allweddeiriau… + Dangos cyfryngau + Dangos yr hyn sydd wedi ei binio + Dim byd cyfatebol wedi ei ganfod! + Tŵtiau wrth gefn am %1$s + Mae %1$s o dŵtiau newydd wedi eu mewnforio + %1$s new notifications have been imported + + Dates descending + Dates ascending + + + Na + Yn unig + Y ddau + + Ni ganfyddwyd unrhyw dŵtiau yn y gronfa data. Defnyddiwch y botwm cydamseru o\'r ddewislen i\'w dychwelyd. + + Data a gofnodwyd + Ond gwybodaeth syml o\'r cyfrifoedd sy\'n cael ei gadw ar y ddyfais. + Mae\'r data yma\'n gwbl gyfrinachol ac ond yn gallu cael ei ddefnyddio gan y rhaglen. + Mae dileu y rhaglen yn cael gwared ar y data yma\'n syth.\n + ⚠ Nid yw\'r manylion mewngofnodi na cyfrineiriau yn cael eu storio. Maent ond yn cael eu defnyddio yn ystod dilysu (SSL) gyda achos. + + Caniatad: + - ACCESS_NETWORK_STATE: Defnyddir er mwyn gweld os yw\'r ddyfais wedi ei gysylltu a rhwydwaith WIFI.\n + - INTERNET: Defnyddir er mwyn gnweud holiadau i achos.\n + - WRITE_EXTERNAL_STORAGE: Defnyddir er mwyn storio cyfryngau neu i symud yr ap ar gerdyn SD.\n + - READ_EXTERNAL_STORAGE: Defnyddir er mwyn ychwanegu cyfryngau i dŵtiau\n + - BOOT_COMPLETED: Defnyddir er mwyn cychwyn y gwasanaeth hysbysu.\n + - WAKE_LOCK: Defnyddir yn ystod y gwasanaeth hysbysu. + + Caniatad API: + - Darllen: Darllen data.\n + - Ysgrifennu: Postio statws ac uwchlwytho cyfryngau ar gyfer statws.\n + - Dilyn: Dilyn, dad-ddilyn, blocio a dad-flocio.\n\n + ⚠ Ceith y rhain ond eu gweithredu pan y mae defnyddiwr yn gwneud cais amdanynt. + + Tracio a Llyfrgelloedd + Nid yw\'r rhaglen hwn yn defnyddio offerynnau tracio (mesur cynulleidfa, adrodd gwallau, a. y. y. b) ac nid oes hysbysebu chwaith.\n\n + Mae defnydd llyfrgelloedd yn gyfyngedig: \n + - Glide: I reoli cyfryngau\n + - Android-Job: I reoli gwasanaethau\n + - PhotoView: I reoli delweddau\n + + Cyfieithiadau o\'r tŵtiau + Mae\'r rhaglen yn caniatau y gallu i gyfieithu toots gan ddefnyddio lleoliad y ddyfais ac API Yandex.\n + Mae gan Yandex bolisi preifatrwydd, mae modd ei weld yma: https://yandex.ru/legal/confidential/?lang=en + + Diolch i: + Hidlo drwy ddefnyddio mynegiadau rheolaidd + Chwilio + Dileu + Nôl mwy o dŵtiau… + + Rhestrau + A ydych chi\'n sicr eich bod eisiau dileu y rhestr hon am byth? + Does dim byd ar y rhestr hwn eto. Wrthi aelodau\'r rhestr gyhoeddi diwedderiadau, byddent yn ymdangos yma. + Ychwanegu i\'r rhestr + Ychwanegu rhestr + Dileu rhestr + Golygu rhestr + Teitl y rhestr newydd + The account was added to the list! + You don\'t have any lists yet! + + Mae %1$s wedi symud i %2$s + Dilysu ddim yn gweithio? + Dyma ambell wiriad all fod yn help:\n\n + - Gwiriwch nad oes camgymeriad sillafu yn enw\'r achos\n\n + - Gwiriwch nad yw eich achos i lawr\n\n + - Os ydych yn defnyddio dilysu dau gam (2FA), defnyddiwch y ddolen ar y gwaelod (unwaith y mae enw\'r achos wedi\'i lenwi)\n\n + - Mae modd defnyddio\'r ddolen yma heb ddefnyddio 2FA\n\n + - Os yw dal ddim yn gweithio, codwch y mater ar Framagit: https://framagit.org/tom79/fedilab/issues + + Mae\'r cyfryngau wedi eu llwytho. Cliciwch yma i\'w harddangos. + Mae\'r weithred yma\'n gallu cymryd amser. Cewch eich hysbysu pan y mae wedi gorffen. + Dal i redeg, arhoswch os gwelwch yn dda… + Allforio statysau + Allforio statysau ar gyfer %1$s + Mae %1$s o dŵtiau o\'r %2$s wedi eu hallforio. + Aeth rhywbeth o\'i le tra\'n allforio data i %1$s + Aeth rhywbeth o le tra\'n allforio data! + Aeth rhywbeth o le tra\'n mewnforio data! + + Procsi + Galluogi procsi? + Gweinydd + Porth + Mewngofnodi + Cyfrinair + Ychwanegu manylion tŵt wrth rannu + Cefnogi\'r ap ar Liberapay + Mae yna wall yn y mynegiad rheolaidd! + Ni ddarganfyddwyd unrhyw ffrydiau ar yr achos yma! + Dileu yr achos yma? + Cyfieithu yn + Dilyn achos + Yr ydych yn dilyn yr achos yma\'n barod! + Dilynwyd yr achos! + Partneriaethau + Gwybodaeth + Cuddio hybiadau o %s + Dangos ar y proffil + Dangos hybiadau o %s + Atal rhag ymddangos ar y proffil + Mae\'r cyfrif hwn nawr yn ymddangos ar y proffil + Nid yw\'r cyfrif hwn yn ymddangos ar y proffil mwyach + Mae hybiadau nawr yn cael eu dangos! + Mae hybiadau nawr yn guddiedig! + Neges preifat + Hidlyddion + Dim hidlyddion i\'w dangos. Mae modd creu un drwy glicio ar y botwm \"+\". + Allweddair neu ymadrodd + Ffrwd gartref + Ffrydiau cyhoeddus + Hysbysiadau + Sgyrsiau + Ceith ei gyfateb heb ystyriaeth i faint lythrennau yn y testun neu rhybudd cynnwys mewn tŵt + Gollwng yn lle cuddio + Bydd y tŵtiau a hidlwyd yn diflannu am byth, hyd yn oed os dileuir yr hidlydd wedyn + Pan y mae allweddair neu ymadrodd yn alffaniwmerig yn unig, bydd ond yn cael ei osod os yw\'n cyfateb i\'r gair cyfan + Gair cyfan + Hidlo cyd-destunnau + Un cyd-destun neu fwy lle dylai\'r hidlydd fod yn berthnasol + Yn dod i ben ar ôl + Dileu hidlydd? + Diweddaru hidlydd + Creu hidlydd + Pwy i ddilyn + Does dim cyfirfoedd wedi\'u rhestru ar hyn o bryd! + Dilyn + Dewis i gyd + Dad-ddewis i gyd + Dilynwyd %s! + Creu y rhestr %s + Ychwanegu cyfrifoedd i\'r rhestr + Ychwanegwyd y cyfrifau i\'r rhestr + Ychwanegu cyfrifau i\'r rhestr + Nid ydych wedi creu rhestr eto. Cliciwch ar y botwm \"+\" i ychwanegu un newydd. + Pwy i ddilyn + API Trunk + Ni ellir dilyn y cyfrif(au) + Nôl cyfrif anghysbell + Ehangu cyfryngau cuddiedig yn awtomatig + New follow + Hybiad newydd + Ffefryn newydd + New Mention + Poll Ended + Tŵt Newydd + Tŵtiau wrth gefn + New posts + Lawrlwytho Cyfryngau + Newid swn hsybysu + Dewis tôn + Caniatau slot amser + Fideos Cymorth + Nôl edefyn anghysbell! + Dim parthau wedi eu blocio! + Dadflocio\'r parth + Ydych chi\'n siŵr eich bod am ddadflocio %s? + Ydych chi\'n siŵr eich bod am flocio %s? + Parthau wedi\'i blocio + Blocio parth + Mae\'r parth wedi\'i flocio + Nid yw\'r parth wedi ei flocio mwyach! + Nôl statws anghysbell + Gwneud sylw + Achos Peertube + Byddwch y cyntaf i adael sylw ar y fideo hwn gyda\'r botwm ar y dde uchaf! + Wedi ei wylio %s gwaith + Hyd: %s + Ychwanegu achos + Nid yw sylwadau wedi eu galluogi ar gyfer y fideo hwn! + Codi penderfyniad + Ffefrynnau Peertube + Mae\'r fideo wedi ei ychwanegu i\'r llyfrnodau! + Mae\'r fideo wedi ei ddileu o\'r llyfrnodau! + Nid oes fideos Peertube yn eich ffefrynnau! + Sianel + Fideos + Sianeli + Defnyddio Emoji One + Gwybodaeth + Dangos rhaglun ym mhob tŵt + Dylunydd UX/UI newydd + Dangos rhaglun fideo + Mae\'r cyfrif id wedi\'i gopïo yn y clipfwrdd! + Newid iaith + Iaith diofyn + Cwtogi tŵtiau hir + Cwtogi tŵtiau dros \'x\' o linellau. Mae sero\'n golygu ei fod wedi ei ddiffodd. + Dangos mwy + Dangos llai + Rheoli tagiau + Mae\'r tag hwn yn bodoli\'n barod! + Mae\'r tag hwn wedi\'i storio! + Mae\'r tag hwn wedi\'i newid! + Mae\'r tag wedi ei ddileu! + Trefnu bŵst + Mae\'r bŵst wedi\'i drefnu! + Dim trefniant bŵst i\'w arddangos! + Trefnu bŵst.]]> + Ffrwd gelf + Agor dewislen + Go back + Logo y rhaglen + Llun proffil + Baner proffil + Cysylltwch a gweinyddwr yr achos + Ychwanegu + Logo MastoHost + Dewiswr emoji + Ail-lwytho + Ehangu\'r sgwrs + Cael gwared ar gyfrif + Dileu\'r path a flociwyd + Custom emoji picker + Chwarae fideo + Tŵt newydd + Delwedd y cerdyn + Cuddio cyfryngau + Favicon + Add description for media (for the visually impaired) + + Byth + 30 munud + 1 awr + 6 awr + 12 awr + 1 diwrnod + 1 wythnos + + Yn y maes hwn mae angen i chi ysgrifennu enw lletywr eich achos.\nEr enghraifft, os wnaethoch chi greu eich cyfrif ar https://mastodon.social\nYsgrifennwch mastodon.social (heb https://)\n + Mae modd i chi ddechrau ysgrifennu llythrennau cychwynol a bydd enwau yn cael eu hawgrymu.\n\n + ⚠ Bydd y botwm Mewngofnodi ond yn gweithio os yw enw\'r achos yn ddilys a bod yr achos yn weithredol! + + Mwy o wybodaeth + + Ieithoedd + Cyfryngau yn unig + Dangos NSFW + Cyfieithiadau Crowdin + Rheolwr Crowdin + Cyfieithu\'r rhaglen + Ynghylch Crowdin + Bot + Achos Pixelfed + Achos Mastodon + Unrhyw un o rhain + Rhain i gyd + Dim un o rhain + Unrhyw un o\'r geiriau hyn (a gofod rhyngddynt) + Yr holl eiriau hyn (a gofod rhyngddynt) + Add some words to filter (space-separated) + Newid enw\'r golofn + Achos Misskey + Nid oes ap sy\'n cefnogi\'r ddolen yma wedi\'i osod ar eich dyfais. + Tanysgrifiadau + Trosolwg + Trendio + Wedi\'i ychwanegu yn ddiweddar + Lleol + Uwchlwytho + Ateb + Dileu sylw + Ydych chi\'n sicr eich bod am ddileu y sylw hwn? + Fideo sgrîn llawn + Modd ar gyfer fideo + Dewiswch y ddogfen er mwyn uwchlwytho + Fy fideo + Teitl + Trwydded + Categori + Iaith + Mae\'r fideo hwn yn cynnwys deunydd aeddfed neu anaddas + Tanysgrifiadau + Diweddaru fideo + Disgrifiad + Mae\'r fideo wedi ei ddiweddaru! + Uwchlwythiad wedi\'i ganslo! + Mae\'r fideo wedi ei uwchlwtho! + Uwchlwytho, arhoswch os gwelwch yn dda… + Cliciwch yma i olygu data\'r fideo. + Dileu fideo + Ydych chi\'n sicr eich bod am ddileu y fideo hwn? + Dangos fideo anaddas i\'r gweithle + Dim fideo i\'w arddangos! + Gadael sylw + Rhannu + Dewiswch fodd amserlennu + O\'r ddyfais + O\'r gweinydd + Tŵtiau (Gweinydd) + Tŵtiau (Dyfais) + Newid + Dangos tŵtiau newydd uwchben y botwm \"Nôl mwy\" + Ffrydiau + Rhyngwyneb + Cysylltiadau + %1$s wneud sylw ar eich fideo %2$s]]> + %1$s yn dilyn eich sianel %2$s]]> + %1$s yn dilyn eich cyfrif]]> + %1$s wedi\'i gyhoeddi]]> + %1$s wedi llwyddo]]> + %1$s wedi methu]]> + %1$s fideo newydd: %2$s]]> + %1$s wedi ei osod ar y rhestr ddu]]> + %1$s wedi ei dynnu o\'r rhestr ddu]]> + Allforio data + Mewnforio Data + Dewiswch ddogfen i\'w fewnforio + Bu gwall tra\'n dewis y ddogfen wrth gefn! + Ychwanegu sylw cyhoeddus + Anfon sylw + Nid oes cyswllt a\'r rhyngrwyd. Mae eich neges wedi ei storio yn y drafftiau. + Testun plaen + HTML + Marcio + Allgofnodi o\'r cyfrif + Oll + Cefnogi\'r ap + Mae Open Collective yn caniatau grwpiau i sefyddlu torf, codi arian a\'u rheoli mewn modd tryloyw. + Copïo\'r ddolen + Cysylltu + Normal + Cryno + Consol + Gosod modd arddangos + Diweddaru\'r Darparwr Diogelwch + Diweddaru y parthau tracio + Ma\'r bas data wedi ei ddiweddaru! + galwadau http wedi\'u blocio gan y rhaglen + Rhestr o alwadau wedi\'u blocio + Cyflwyno + Mae\'r bas data wedi ei allforio! + Hashnodau Nodedig + Hidlo ffrwd drwy ddefnyddio tagiau + Dim tagiau + Hide the \"delete\" button in the notification tab + Attach an image when sharing a URL + + Poll + Polls + Create a poll + Choice 1 + Choice 2 + Choice %d + You need two choices at least for the poll! + Done + end at %s + Refresh poll + Vote + A poll you have voted in has ended + A poll you tooted has ended + Customize + Categories + Time slot + Advanced + Display \'new\' badge on unread toots + Peertube + Move timeline + Hide timeline + Reorder timelines + List permanently deleted + Followed instance removed + Pinned tag removed + Undo + You need to keep two visible tabs! + Reorder timelines + Main timelines can only be hidden! + BBCode + Always mark media as sensitive + GNU instance + Cached status + Forward tags in replies + Long press to store media + Blur sensitive media + Display timelines in a list + Display timelines + Mark bot accounts in toots + Manage tags + Remember the position in Home timeline + History + Playlists + Display name + You don\'t have any playlists. Tap on the \"+\" icon to add a new playlist + You must provide a display name! + The channel is required when the playlist is public. + Create a playlist + There is nothing in this playlist yet. + redo + Gallery + Emoji + Sticker + Eraser + Text + Filter + Brush + Are you sure you want to exit without saving the image? + Discard + Saving… + Image Saved Successfully! + Failed to save Image + Opacity + Enable photo editor + Add a poll item + Remove last poll item + Mute conversation + Unmute conversation + The conversation is no longer muted! + The conversation is muted + Open application features + Timed mute + Mention the account + Refresh cache + Mention the status + News + General + Regional + Art + Journalism + Activism + Gaming + Technology + Adult content + Furry + Food + Logo of the instance + Something went wrong when checking available instances! + Join Mastodon + Choose an instance by picking up a category, then tap on a check button. + Choose an instance by tapping on a check button. + %1$s users + Confirm password + I agree to %1$s and %2$s + server rules + terms of service + Sign up + This instance works with invitations. Your account will need to be manually approved by an administrator before being usable. + Please, fill all the fields! + Passwords don\'t match! + The email doesn\'t seem to be valid! + Your username will be unique on %1$s + You will be sent a confirmation e-mail + Use at least 8 characters + Password should contain at least 8 characters + Username should only contain letters, numbers and underscores + Account created! + Your account has been created!\n\n + Think to validate your email within the 48 next hours.\n\n + You can now connect your account by writing %1$s in the first field and tap on Connect.\n\n + Important: If your instance required validation, you will receive an email once it is validated! + + Save the message in drafts? + Administration + Reports + No reports to display! + Reconnect the account + The application failed to access the admin features. You might need to reconnect the account for having the correct scope. + Unresolved + Remote + Active + Pending + Disabled + Silenced + Suspended + Permissions + Email status + Login status + Joined + Most recent IP + Warn + Disable + Silence + Notify the user per e-mail + Custom warning + User + Moderator + Administrator + Confirmed + Not confirmed + Reported statuses + Account + Undo silence + Undo disable + Suspend + Undo suspend + The account is silenced! + The account is no longer silenced! + The account is suspended! + The account is no longer suspended! + The account is disabled! + The account is no longer disabled! + The account has been warned! + Display the admin menu + Display the admin feature in statuses + Allow + The account is approved! + The account is rejected! + Assign to me + Unassign + Mark as resolved + Mark as unresolved + Empty content! + Display Fedilab features button + The application needs to access audio recording + Voice message + Enable quick reply + The account you are replying might not see your message! + If disabled, the app will always load last statuses + If disabled, sensitive media will be hidden with a button + Store media in full size with a long press on previews + Add an ellipse button at the top right for listing all tags/instances/lists + During the time slot, the app will send notifications. You can reverse (ie: silent) this time slot with the right spinner. + Display a Fedilab button below profile picture. It is a shortcut for accessing in-app features. + Allow to reply directly in timelines below statuses + Previews will not be cropped in timelines + Allow to play embedded videos directly in timelines + Allow to reverse the way to read statuses that are displayed once tapping the fetch more button + This option allows to support recent cipher suites. It is useful for older Android devices or if you cannot connect to your instance. + Exclusively for Peertube videos. Switch this mode if you cannot play them. + These tags will allow to filter statuses from profiles. You will have to use the context menu for seeing them. + Automatically insert a line break after the mention to capitalize the first letter + Allow content creators to share statuses to their RSS feeds + Compose + Maximum retry times when uploading media + Create a new Folder here + Enter the folder name + Please enter a valid folder name + This folder already exists.\n Please provide another name for the folder + Select + Default Directory + Folder + Create folder + Display a toast message after an action has been completed (boost, fav, etc.)? + Muted instances have been exported! + Add an instance + Export instances + Import instances + Crash reports + Enable crash reports + If enabled, a crash report will be created locally and then you will be able to share it. + Mae Fedilab wedi stopio :( + Mae modd i chi anfon adroddiad crash ata\'i drwy ebost. Bydd yn help i\'w ddatrys. :)\n\nMae modd i chi ychwanegu cynnwys ychwanegol! Diolch! + Use the wysiwyg + When enabled, you will be able to format your text easily with tools. + Statistics + Total statuses + Number of boosts + Number of favourites + Number of mentions + Number of follows + Number of polls + Number of replies + Number of statuses + Statuses + Visibility + Number with media + Number with sensitive media + Number with CW + First status date + Last status date + First notification date + Last notification date + Frequency + %s statuses per day + %s notifications per day + Date range + Groups + No groups! + Disable custom animated emojis + Charts + Display charts + The application collects your local data, please wait… + Backup + Auto backup statuses + This option is per account. It will launch a service that will automatically store your statuses locally in the database. That allows to get statistics and charts + Auto backup notifications + This option is per account. It will launch a service that will automatically store your notifications locally in the database. That allows to get statistics and charts + Report account + Send an invitation + Your instance does not allow to register a new account! + + %d votes + %d vote + %d votes + %d votes + %d votes + %d votes + + + %d voters + %d voter + %d voters + %d voters + %d voters + %d voters + + + Single choice + Multiple choices + + + 5 minutes + 30 minutes + 1 hour + 6 hours + 1 day + 3 days + 7 days + + + Webview + Ffrwd uniongyrchol + + For joining my instance \"%1$s\", you can download Fedilab:\n\nF-Droid: %2$s\nGoogle: %3$s\n\nThen open the link below with Fedilab and create your account :)\n\n%4$s + + Your poll can\'t have duplicated options! + For all accounts + Database cache + Clear your home timeline cache + Clear your cached statuses + Clear your bookmarks + Files in cache + Total notifications + Hide menu items + Fedilab is running live notifications + For %1$s accounts with %2$s events + Live notifications for %1$s + Live notifications will be enabled for this account. + Clear cache when leaving + The cache (media, cached messages, data from the built-in browser) will be automatically cleared when leaving the application. + Do you want to unfollow this account? + Show confirmation dialog before unfollowing + Replace Youtube with Invidio.us + Invidious is an alternative front-end to YouTube + Enter your custom host or leave blank for using invidious.snopyta.org + Replace Twitter with Nitter + Nitter is an open source alternative Twitter front-end focused on privacy. + Enter your custom host or leave blank for using nitter.net + Replace Instagram with Bibliogram + Bibliogram is an open source alternative Instagram front-end focused on privacy. + Enter your custom host or leave blank for using bibliogram.art + Replace Reddit with Libreddit + Libreddit is an open source alternative Reddit front-end focused on privacy. + Enter your custom host or leave blank for using libredd.it + Replace Medium links + Replace medium.com links with an open source alternative front-end focused on privacy. + Default: scribe.rip + Replace Wikipedia links + Replace Wikipedia link with an open source alternative front-end focused on privacy. + Default: wikiless.org + Hide Fedilab notification bar + For hiding the remaining notification in the status bar, tap on the eye icon button then uncheck: \"Display in status bar\" + Use a push notifications system for getting notifications in real time. + No live notifications + Live notifications + Notifications will be fetched every 15 minutes. + Add notes + Notes for the account + Allow to compress large photos into smaller sized photos with very less or negligible loss in quality of the image. + Allow compressing videos while maintaining their quality. + The app is compressing the media, it can be quite long… + Change app icon + Tap to change the app icon + Post + Visibility of the post + Tap here to add photos + Accepted Formats: jpeg, png, gif \n\nMax File Size: 15 MB \n\nAlbums can contain up to 4 photos or videos + Upload media + Add an optional caption + The app received a very long error message from the API %1$s + Message preview + Add mentions in each message + Fetching conversation + Order by + Title for the video + Join Peertube + I am at least 16 years old and agree to the %1$s of this instance + Links + Change the color of links (URLs, mentions, tags, etc.) in messages + Reblogs header + Change the color of display name at the top of messages + Change the color of the user name at the top of messages + Change the color of the header for reblogs + Posts + Background color of posts in timelines + Reset colors + Tap here to reset all your custom colors + Reset + Icons + Color of bottom icons in timelines + Pin this tag + Logo of the instance + Edit profile + Make an action + Translation + Image preview + Text color + Change the text color in pots + Apply changes + You need to restart the application to apply changes + Restart + Use a custom theme + Allow to override colors of the selected theme above + Theming + Store before + The theme was exported + The theme has been successfully exported in CSV + Apply the primary color to the status bar + Status bar color + Restore a default theme + Import a theme + Tap here to import a theme from a previous export + Export the theme + Tap here to export the current theme + An error occurred when selecting the theme file + Theme Picker + Select a pre-installed theme + Themes + Apply the primary color to the navigation bar + Navigation bar color + The underlying color of the app’s content. + Background color + Accents select parts of the UI. + Accent color + Displayed most frequently across your app. + Primary color + Export bookmarks to the instance + Import bookmarks from the instance + User count + Status count + Instance count + Blocked + End in %s + What\'s new in %s + You can follow my account for updates + This instance is not available on https://instances.social + Display full link + Share link + The URL has been copied to the clipboard + Open with another app + Check redirect + This URL does not redirect + %1$s \n\nredirects to\n\n %2$s + Change the user agent + Set a custom user agent or leave blank + Allows to customize the user agent used for api calls or with the built-in browser. + Remove UTM parameters + The app will automatically remove UTM parameters from URLs before visiting a link. + Trends + Trending now + %d people talking + Twitter accounts (via Nitter) + Twitter usernames space separated + Identity proofs + Verified identity + Verified by %1$s (%2$s) + Delete the notification + Display more options + It is a Pixelfed story + Upload a media, it will be automatically added to your Pixelfed story. + Media successfully added to your story! + Action disabled + Unfollow + Something went wrong, please check your download directory in settings. + Announcements + No announcements! + Add a reaction + Use your favourite browser inside the app. Uncheck this feature to open links externally. + Video cache in MB, zero means no cache. + Watermarks + Automatically add a watermark at the bottom of pictures. The text can be customized for each account. + No distributors found! + You need a distributor for receiving push notifications.\nYou will find more details at %1$s.\n\nYou can also disable push notifications in settings for ignoring that message. + Select a distributor + diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml new file mode 100644 index 000000000..5085d3865 --- /dev/null +++ b/app/src/main/res/values-da/strings.xml @@ -0,0 +1,1142 @@ + + + Åbn menuen + Luk menuen + Om + Om denne instans + Privatliv + Cache + Log ud + Log ind + + Luk + Ja + Nej + Annullér + Download + Download %1$s + Medie gemt + Fil: %1$s + Adgangskode + E-mail + Konti + Toots + Tags + Gem + Gendan + Ingen resultater! + Instans + Instans: mastodon.social + Bruger nu kontoen: %1$s + Tilføj en konto + Indhold af dette toot er kopieret til udklipsholder + URL\'en til dette toot er kopieret til udklipsholder + Skift + Vælg et billede… + Ryd + Kamera + Slet alle + Oversæt dette toot. + Planlæg + Tekst- og ikonstørrelser + Skift aktuel skriftstørrelse: + Skift aktuel ikonstørrelse: + Næste + Foregående + Åbn med + Godkend + Medier + Del med + Delt via Fedilab + Svar + Brugernavn + Kladder + Favoritter + Nye følgere + Omtaler + Boosts + Vis boosts + Vis svar + Åbn i browser + Oversæt + Vent nogle sekunder før du foretager denne handling. + + Hjem + Lokal tidslinje + Fælles tidslinje + Valgmuligheder + Favoritter + Kommunikation + Tavse brugere + Blokerede brugere + Notifikationer + Følgeanmodninger + Indstillinger + Slet en konto + Slet kontoen %1$s fra app\'en? + Send en e-mail + Tryk på stien for at ændre den + Mislykkedes! + Planlagte toots + Informationen nedenfor afspejler måske ikke brugerens profil fuldstændigt. + Indsæt emoji + App\'en indsamlede ikke tilpassede emojis i øjeblikket. + Push-notifikationer + Sikker på, at du vil logge ud? + Sikker på, at du vil logge ud af @%1$s@%2$s? + + Ingen toot at vise + Ingen historier at vise + Historier + Boostet af %1$s + Føj dette toot til dine favoritter? + Fjern dette toot fra dine favoritter? + Boost dette toot? + Fjern boost fra dette toot? + Fastgør dette toot? + Frigør dette toot? + Gør tavs + Blokér + Rapportér + Fjern + Kopiér + Del + Nævn + Tidsbegrænset tavshed + Slet og omformulér + + Gør denne konto tavs? + Blokér denne konto? + Anmeld dette toot? + Blokér dette domæne? + Fjern \"Gør tavs\" for denne konto? + Afblokér denne konto? + + + Notificér + Undertryk + + + Fjern dette toot? + Slet og omformulér dette toot? + + Bogmærker + Tilføj bogmærke + Fjern bogmærke + Ingen bogmærker at vise + Status er føjet til bogmærker! + Status er fjernet fra bogmærker! + + %d s + %d m + %d t + %d d + + %d sekund + %d sekunder + + + %d minut + %d minutter + + + %d time + %d timer + + + %d dag + %d dage + + + Advarsel + Hvad har du i tankerne? + TOOT! + QUEET! + cw + Skriv et toot + Besvar et toot + Skriv et queet + Besvar et queet + Vælg et medie + En fejl opstod under valg af medie! + Slette dette medie? + Dit toot er tomt! + Toot\'ets synlighed + Toots standardsynlighed: + Toot\'et er sendt! + Du besvarer flg. toot: + Følsomt indhold? + + Udgiv på offentlige tidslinjer + Udgiv ikke på offentlige tidslinjer + Udgiv kun til følgere + Udgiv kun til nævnte brugere + + Ingen udkast! + Vælg et toot + Vælg en konto + Vælg nogle konti + Fjern udkast? + Tryk på knappen for at vise det oprindelige toot + Beskriv for synshandicappede + + Ingen beskrivelse tilgængelig! + + Version %1$s + Udvikler: + Licens: + GNU GPL V3 + Kildekode: + Oversættelse af toots: + Søg instanser: + Ikondesigner: + + Samtale + + Ingen konto at vise + Ingen følgeanmodninger + Toots \n %1$s + Følger \n %1$s + Følgere \n %1$s + Fastgjort \n %d + Godkend + Afvis + + Ingen planlagte toots at vise! + Skriv et toot og vælg derefter Planlæg fra topmenuen. + Slet planlagt toot? + Medie: %d + Toot\'et er nu planlagt! + Det planlagte tidspunkt skal ligge efter det aktuelle tidspunkt! + Strømsparetilstand er aktiveret! Den fungerer måske ikke som forventet. + + Tidspunkt for at gøre tavs skal være om mindst et minut. + %1$s er blevet gjort tavs indtil %2$s.\n Du kan fjerne dette fra profilensiden. + %1$s er gjort tavs indtil %2$s.\n Tryk hér for at fjerne det igen. + + Ingen notifikationer at vise + nævnte dig + skrev en ny besked + boostede din status + favoriserede din status + fulgte dig + bad om at måtte følge dig + + og en anden notifikation + og %d andre notifikationer + + + %d Like + %d Likes + + Slet en notifikation? + Slet alle notifikationer? + Notifikationen er slettet! + Alle notifikationer er slettet! + + Følger + Følgere + Fastgjort + + Kan ikke hente klient-ID! + Kan ikke tilslutte instansdomæne! + Ingen Internetforbindelse! + Kontoen blev blokeret! + Kontoen er ikke længere blokeret! + Kontoen blev gjort tavs! + Kontoen er ikke længere tavs! + Kontoen blev fulgt! + Kontoen følges ikke længere! + Toot\'et blev boostet! + Toot\'et boostes ikke længere! + Toot\'et blev føjet til dine favoritter! + Toot\'et blev fjernet fra dine favoritter! + Toot\'et blev anmeldt! + Toot\'et er slettet! + Toot\'et er fastgjort! + Toot\'et er frigjort! + Ups! En fejl opstod! + En fejl opstod! Instansen returnerede ikke en godkendelseskode! + Domænet for instansen ser ud til at være ugyldigt! + En fejl opstod under kontoskift! + En fejl opstod under søgning! + Profildata er gemt! + Ingen handling kan udføres + Mediet er gemt! + En fejl opstod under oversættelsen! + Oversættelser er deaktiveret i indstillinger + Kladde gemt! + Sikker på, at denne instans tillader dette antal tegn? Normalt er denne værdi tæt på 500 tegn. + Toots-synlighed er ændret for kontoen %1$s + + Antal toots pr. indlæsning + Altid + Wi-Fi + Spørg + Indlæs medierne + Indlæs billederne + Vis mere… + Vis mindre… + Følsomt indhold + Deaktivér GIF-avatarer + Sti: + Gem kladder automatisk + Tilføj URL på medier i toots + Notificér, når en person følger dig + Notificér, når nogen booster din status + Notificér, når nogen favoriserer din status + Notificér, når nogen nævner dig + Advisér, når en afstemning er afsluttet + Advisér om nye indlæg + Vis bekræftelsesdialog inden boosting + Vis bekræftelsesdialog inden favorittilføjelse + Notificér kun via Wi-Fi + Notificér? + Tavse Notifikationer + NSFW-visningstimeout (sekunder, 0 betyder slået fra) + Mediebeskrivelses-timeout (sekunder, 0 = Fra) + Redigér profil + Tilpasset deling + Din tilpassede delings-URL… + Bio… + Lås konto + Gem ændringer + Vælg et overskriftsbillede + Tilpas forhåndsvisningsbilleder + Opdel automatisk toots i svar, når tegnantal overstiger: + Du har nået de tilladte 160 tegn! + Du har nået de tilladte 30 tegn! + Mellem + og + Tiden skal være senere end %1$s + Tiden skal være tidligere end %1$s + Starttid + Sluttid + Benyt indbygget browser + Tilpassede faner + Aktivér JavaScript + Udvid automatisk cw + Tillad tredjeparts-cookies + Din API-nøgle, kan være tom for Yandex + + Mørk + Lys + Sort + + Sæt LED-farve: + + Blå + Cyan + Magenta + Grøn + Rød + Gul + Hvid + + Følg + Afblokér + Gør tavs + Fjern \"Gør tavs\" + Forespørgsel sendt + Følger dig + Søg + Begyndelsesbogstav med stort for svar + Ændr billedstørrelse + Skalér videoer + + Push-notifikationer + Bekræft de push-notifikationer, du ønsker at modtage. + Du kan senere slå disse notifikationer til/fra i indstillinger (fanen Notifikationer). + + + Ryd Cache + Cachen indeholder %1$s data.\n\nVil du slette dataene? + Mb + Cachen er ryddet! %1$s er frigivet + + Titel + Titel… + Beskrivelse + Søgeord + Søgeord… + + Synkronisér + Filtrér + Dine toots + Dine notifikationer + Offentlig + Ikke listet + Privat + Direkte + Nogle søgeord… + Vis medier + Vis fastgjorte + Ingen matchende resultater! + Sikkerhedskopiér toots fra %1$s + %1$s nye toots er importeret + %1$s nye notifikationer er blevet importeret + + Dato, faldende + Dato, stigende + + + Nej + Kun + Begge + + Ingen toots fundet i databasen. Benyt synkroniseringsknappen i menuen for at hente dem. + + Gemte data + Kun grundlæggende oplysninger fra konti lagres på enheden. + Disse data er strengt fortrolige og kan kun anvendes af app\'en. + Sletning af app\'en fjerner straks disse data.\n + ⚠ Loginoplysninger lagres aldrig, da de alene benyttes ifm. en sikker godkendelse (SSL) med en instans. + + Tilladelser: + - ACCESS_NETWORK_STATE: Benyttes til at fastslå, om enheden er tilsluttet et Wi-Fi netværk.\n + - INTERNET: Benyttes ved forespørgsler til en instans.\n + - WRITE_EXTERNAL_STORAGE: Benyttes til lagring af medier eller til at flytte app\'en til et SD-kort.\n + - READ_EXTERNAL_STORAGE: Benyttes til at tilføje medier til toots.\n + - BOOT_COMPLETED: Benyttes til starte notifikationstjenesten.\n + - WAKE_LOCK: Benyttes, når notifikationstjenesten kører. + + API-tilladelser: + - Read: Læse data.\n + - Write: Komme med statusindlæg og uploade medier til statusser.\n + - Follow: Følge, stoppe med at følge, blokere, afblokere.\n\n + ⚠ Disse handlinger udføres kun på brugerforlangende. + + Sporing og Biblioteker + App\'en benytter ikke sporingsværktøjer (publikumsmåling, fejlrapportering mv.), og den indeholder ingen reklamer.\n\n + Brugen af biblioteker er minimeret: \n + - Glide: Til mediehåndtering\n + - Android-Job: Til tjenestehåndtering\n + - PhotoView: Til billedhåndtering\n + + Oversættelse af toots + App\'en giver mulighed for at oversætte toots ved brug af enhedslokaliseringen og Yandex-API\'en.\n + Yandex har en passende fortrolighedspolitik, der kan findes hér: https://yandex.ru/legal/confidential/?lang=en + + Tak til: + + Bortfiltrér efter regulære udtryk + Søg + Slet + Hent flere toots… + + Lister + Sikker på, at du vil slette denne liste permanent? + Der er intet i denne liste endnu. Når listemedlemmer poster nye statusser, vil de fremgå her. + Føj til liste + Tilføj liste + Slet liste + Redigér liste + Ny listetitel + Kontoen blev føjet til listen! + Du har ingen lister endnu! + + %1$s er flyttet til %2$s + Kan ikke godkendes? + Her er nogle tjek, som måske hjælper:\n\n + - Tjek, at der ikke er stavefejl i instansnavnet\n\n + - Tjek, at din instans ikke er utilgængelig\n\n + - Benytter du tofaktorgodkendelse, (2FA), så benyt linket (når instansnavnet er udfyldt) i bunden\n\n + - Du kan også benytte linket uden brug af 2FA\n\n + - Fungerer det stadig ikke, så opert en problemrapport på Framagit via https://framagit.org/tom79/fedilab/issues + + Medie er indlæst. Tryk hér for at vise det. + Denne handling kan tage ganske lang tid. Du underrettes, når den er færdig. + Kører stadig, vent venligst… + Eksportér statusser + Eksportér statusser for %1$s + %1$s toots ud af %2$s er eksporteret. + Noget gik galt under dataeksporten for %1$s + Noget gik galt under dataeksporten! + Noget gik galt under dataimporten! + + Proxy + Aktivér proxy? + Vært + Port + Brugernavn + Adgangskode + Tilføj toot-detaljer ved deling + Støt app\'en på Liberapay + Der er en fejl i det regulære udtryk! + Ingen tidslinjer fundet for denne instans! + Slet denne instans? + Oversæt til + Følg instans + Du følger allerede denne instans! + Instansen følges nu! + Partnerskaber + Information + Skjul boosts fra %s + Vis på profil + Vis boosts fra %s + Vis ikke på profil + Kontoen fremhæves nu på profilen + Kontoen fremhæves ikke længere på profilen + Boosts vises nu! + Boosts skjules nu! + Direkte besked + Filtre + Ingen filtre at vise. Du kan oprette et ved at trykke på \"+\"-knappen. + Søgeord eller -udtryk + Hjem-tidslinje + Offentlige tidslinjer + Notifikationer + Samtaler + Matcher uafhængigt af minuskler/versaler i tekst eller inholdsadvarsel i et toot + Forkast i stedet for at skjule + Filtrerede toots forsvinder uigenkaldeligt, selv hvis filtret senere fjernes + Er søgeordet/-sætningen rent alfanumerisk, anvendes det/den kun ved match af hele ordet + Helt ord + Filterkontekster + Én eller flere kontekster, hvortil filteret skal benyttes + Udløber efter + Slet filter? + Opdatér filter + Opret filter + Hvem vil jeg følge + Ingen konti opført i øjeblikket! + Følg + Vælg alle + Fravælg alle + %s følges! + Opretter listen %s + Tilføjer konti til listen + Konti blev føjet til listen + Føjer konti til listen + Du har endnu ikke oprettet en liste. Tryk på \"+\"-knappen for at oprette en ny. + Hvem man følger + Trunk API + Konto/konti kan ikke følges + Henter fjernkonto + Udvid automatisk skjulte medier + Ny følger + Nyt boost + Ny favorit + Ny omtale + Afstemning afsluttet + Nyt toot + Toot-sikkerhedskopiering + Nye indlæg + Medie-download + Skift notifikationslyd + Vælg lyd + Aktivér tidsinterval + Videoguider + Henter fjerntråd! + Ingen blokerede domæner! + Afblokér domæne + Sikker på, at du vil afblokere %s? + Sikker på, at du vil blokere %s?\n\n\Du vil ikke se indhold fra domænet i nogen offentlig tidslinje eller i dine notifikationer. Dine følgere fra domænet fjernes. + Blokerede domæner + Blokér domæne + Domænet er blokeret + Domænet er ikke længere blokeret! + Henter fjernstatus + Kommentar + Peertube instans + Vær den første til at skrive en kommentar til denne video via den øverste, højre knap! + %s visninger + Varighed: %s + Tilføj en instans + Kommentarer er ikke aktiveret for denne video! + Vælg en opløsning + Peertube favoritter + Videoen er føjet til bogmærker! + Videoen er fjernet fra bogmærker! + Der er ingen Peertube videoer i dine favoritter! + Kanal + Videoer + Kanaler + Brug Emoji One + Information + Forhåndsvis alle toots + Ny UI/UX-designer + Vis videoforhåndsvisninger + Konto-ID\'en kopieret til udklipsholder! + Skift sprog + Standardsprog + Afkort lange toots + Afkort toots på flere end \'x\' linjer. Nul betyder deaktiveret. + Vis mere + Vis mindre + Håndtér tags + Tag\'et findes allerede! + Tag\'et er gemt! + Tag\'et er ændret! + Tag\'et er slettet! + Planlæg boost + Boostet er planlagt! + Ingen planlagte boosts at vise! + Planlæg boost.]]> + Kunsttidslinje + Åbn menu + Gå tilbage + App\'ens logo + Profilbillede + Profilbanner + Kontakt admin for instansen + Tilføj ny + MastoHost logo + Emoji-vælger + Opfrisk + Udvid samtalen + Fjern en konto + Slet det blokerede domæne + Tilpasset emoji-vælger + Afspil video + Nyt toot + Billede på kortet + Skjul medie + Favikon + Tiløj en mediebeskrivelse (til synshæmmede) + + Aldrig + 30 minutter + 1 time + 6 timer + 12 timer + 1 dag + 1 uge + + I dette felt skal du angive din instans\' værtsnavn.\nOprettede du f.eks. din konto på https://mastodon.social\nSå angiv mastodon.social (uden https://)\n + Du kan begynde at skrive, hvorefter navneforslag vises.\n\n + ⚠ Forbind-knappen vil kun fungere, hvis instansnavnet er korrekt og instansen er online! + + Mere information + + Sprog + Kun medier + Vis NSFW + Crowdin-oversættelser + Crowdin-administrator + Oversættelse af app\'en + Om Crowdin + Bot + Pixelfed instans + Mastodon instans + Enhver af disse + Alle disse + Ingen af disse + Ethvert af disse ord (separeret med mellemrum) + Alle disse ord (separeret med mellemrum) + Tilføj ord til filter (separeret med mellemrum) + Skift kolonnenavn + Misskey instans + Ingen app på din enhed understøtter dette link. + Abonnementer + Oversigt + Populære + Nyligt tilføjet + Lokalt + Upload + Besvar + Slet kommentar + Sikker på, at du vil slette denne kommentar? + Fuldskærmsvideo + Videotilstande + Vælg fil til upload + Mine videoer + Titel + Licens + Kategori + Sprog + Denne video har erotisk eller anstødeligt indhold + Aktivér video kommentarer + Opdatér video + Beskrivelse + Denne video er opdateret! + Upload annulleret! + Videoen er uploadet! + Uploader, vent venligst… + Tryk hér for at redigere videodata. + Slet video + Sikker på, at du vil slette denne video? + Vis NSFW-videoer + Ingen videoer at vise! + Skriv en kommentar + Del + Vælg en planlægningstilstand + Fra enhed + Fra server + Toots (Server) + Toots (Enhed) + Redigér + Vis nye toots over \"Hent flere\"-knappen + Tidslinjer + Brugerflade + Kontakter + %1$s kommenterede din video %2$s]]> + %1$s følger din kanal %2$s]]> + %1$s følger din konto]]> + %1$s er blevet offentliggjort]]> + %1$s lykkedes]]> + %1$s mislykkedes]]> + %1$s har udgivet en ny video: %2$s]]> + %1$s er blevet sortlistet]]> + %1$s er fjernet fra sortlisten]]> + Eksportér data + Importér Data + Vælg filen, der skal importeres + Der opstod en fejl under valg af sikkerhedskopifil! + Tilføj en offentlig kommentar + Send kommentar + Der er ingen internetforbindelse. Din besked er blevet gemt i kladder. + Simpel tekst + HTML + Markdown + Log ud af konto + Alle + Støt app\'en + Open Collective gør det muligt for grupper hurtigt at oprette en samarbejdsgruppe, rejse midler og håndtere dem på en transparent måde. + Kopiér link + Forbind + Normal + Kompakt + Konsol + Sæt visningstilstand + Opdatér Sikkerhedsleverandør + Opdatér sporingsdomæner + Sporingsdatabasen er opdateret! + http-kald blokeret af app\'en + Liste over blokerede kald + Indsend + Databasen blev eksporteret! + Fremhævede hashtags + Filtrér tidslinje med tags + Ingen tags + Skjul \"Slet\"-knappen i notifikationsfanen + Vedhæft et billede ved deling af en URL + + Afstemning + Afstemninger + Opret en afstemning + Valgmulighed 1 + Valgmulighed 2 + Valgmulighed %d + Du behøver minimum to valgmuligheder i en afstemning! + Udført + slutter %s + Opfrisk afstemning + Stem + En afstemning, hvori du har stemt, er slut + En afstemning, du tootede, er slut + Tilpas + Kategorier + Tidsinterval + Avanceret + Vis \'Ny\'-mærkat på ulæste toots + Peertube + Flyt tidslinje + Skjul tidslinje + Omarrangér tidslinjer + Listen slettet permanent + Fulgt instans fjernet + Fastgjort tag fjernet + Fortryd + Du skal beholde to synlige faner! + Omarrangér tidslinjer + Hovedtidslinjer kan kun skjules! + BBkode + Markér altid medier som følsomme + GNU instans + Cachelagret status + Videresend tags i svar + Langt tryk for at gemme medier + Skjul følsomme medier + Vis tidslinjer i en liste + Vis tidslinjer + Markér bot-konti i toots + Håndtér tags + Husk position i Hjem-tidslinje + Historik + Afspilningslister + Visningsnavn + Du har ingen afspilningslister. Tryk på \"+\"-ikonet for at tilføje en ny + Du skal angive et visningsnavn! + Kanalen er obligatorisk, når afspilningslisten er offentlig. + Opret afspilningsliste + Afspilningslisten er tom. + gendan + Galleri + Emoji + Klistermærke + Viskelæder + Tekst + Filter + Pensel + Sikker på, at du vil afslutte uden at gemme billedet? + Kassér + Gemmer… + Billede gemt! + Mislykkedes at gemme billede + Opacitet + Aktivér fotoredigeringsværktøj + Tilføj et afstemningsemne + Fjern seneste afstemningsemne + Gør samtale tavs + Genaktivér samtale + Samtalen er ikke længere gjort tavs! + Samtalen er gjort tavs + Åbn app-funktioner + Planlagt tavshed + Nævn kontoen + Opfrisk cache + Nævn statussen + Nyheder + Generelt + Regionalt + Kunst + Journalistik + Aktivisme + Gaming + Teknologi + Erotisk indhold + Furry + Mad + Logo for instansen + Noget gik galt ved søgning efter tilgængelige instanser! + Tilmeld dig Mastodon + Vælg en instans fra en af kategorierne ved at klikke på dens afkrydsningsfelt. + Vælg en instans ved at trykke på tjekkknappen. + %1$s brugere + Bekræft adgangskode + Jeg accepterer %1$s og %2$s + serverregler + tjenestevilkår + Tilmeld + Denne instans fungerer vha. invitationer. Din konto skal først manuelt godkendes af en administrator, før den kan tages i brug. + Udfyld venligst alle felter! + Adgangskoder matcher ikke! + E-mailen ser ud til at være ugyldig! + Dit brugernavn vil være unikt på %1$s + Du vil blive tilsendt en bekræftelses e-mail + Brug mindst 8 tegn + Adgangskoden skal udgøre mindst 8 tegn + Brugernavn må kun indeholde bogstaver, tal og understregninger + Konto oprettet! + Din konto er nu oprettet!\n\n + Husk at bekræfte din e-mail indenfor de næste 48 timer.\n\n + Du kan nu forbinde til din konto ved at angive %1$s i det første felt og trykke på Forbind.\n\n + Vigtigt: Kræver din instans bekræftelse, vil du modtage en e-mail, så snart den er bekræftet! + + Gem beskeden i kladder? + Administration + Rapporter + Ingen rapporter at vise! + Genforbind kontoen + App\'en kunne ikke tilgå admin-funktioner. Du skal muligvis genforbinde kontoen, for at kunne tilgå det relevante område. + Uløst + Fjernsystem + Aktive + Afventer + Deaktiveret + Gjort tavs + Sat i bero + Tilladelser + E-mail status + Login-status + Medlem + Seneste IP + Advar + Deaktivér + Gør tavs + Underrette brugeren pr. e-mail + Tilpasset advarsel + Bruger + Moderator + Administrator + Bekræftet + Ikke bekræftet + Rapporterede statusser + Konto + Fortryd gjort tavs + Fortryd Deaktivér + Udsæt + Fortryd Udsæt + Kontoen er gjort tavs! + Kontoen er ikke længere gjort tavs! + Kontoen er suspenderet! + Kontoen er ikke længer suspenderet! + Kontoen er deaktiveret! + Kontoen er ikke længere deaktiveret! + Kontoen er givet en advarsel! + Vis admin-menuen + Vis admin-funktioner i statusser + Tillad + Kontoen er godkendt! + Kontoen er afvist! + Tildele mig + Tildel ikke længere + Markér som løst + Markér uløst + Intet indhold! + Vis Fedilab-funktionsknap + App\'en behøver adgang til lydoptagelse + Talebesked + Aktivér hurtig visning + Kontoen, du besvarer, kan måske ikke se din besked! + Hvis deaktiveret, indlæser app\'en altid seneste statusser + Hvis deaktiveret, skjules følsomme medier altid med en knap + Lagr medier i fuld størrelse med et langt tryk på forhåndsvisninger + Tilføj en ellipseknap øverst til højre for at få vist alle tags/forekomster/lister + I løbet af tidperioden vil app\'en vil sende notifikationer. Du kan ændre (dvs. gøre tavs) denne tidsperiode med den højre kontrol. + Vis en Fedilab-knap nedenfor profilbilledet. Det er en genvej for adgang til in-app funktioner. + Tillad direkte besvarelse fra tidslinjer under statusser + Forhåndsvisninger beskæres ikke i tidslinjer + Tillad afspilning af indlejrede videoer direkte fra tidslinjer + Tillader, spejlvendingg af måden, hvorpå statusser, som hentes når du klikker på Hent flere-knappen, læses + Indstillingen muliggør understøttelse af tidligere cipher suites. Det er nyttigt på ældre Android-enheder, eller hvis du ikke kan oprette forbindelse til din instans. + Kun til Peertube-videoer. Skift denne tilstand, hvis du ikke kan afspille dem. + Disse tags vil muliggøre at filtrere statusser fra profiler. Du skal benytte kontekstmenuen for at se dem. + Auto-indsæt et linjeskift efter nævningen, for at gøre første bogstav til en versal + Lader indholdskreatører deler statusser til deres RSS-feeds + Skriv + Maks. antal forsøg ved medie-upload + Opret en ny mappe hér + Angiv mappenavnet + Angiv et gyldigt mappenavn + Denne mappe findes allerede.\nAngiv et andet navn til mappen + Vælg + Standartmappe + Mappe + Opret mappe + Vis en toast-besked efte fuldførelsen af en handling (boost, fav mv.)? + Forstummede instanser er eksporteret! + Tilføj en instans + Eksportér instanser + Importér instanser + Nedbrudsrapporter + Aktivér nedbrudsrapporter + Hvis aktiveret, vil en nedbrudsrapport blive oprettet lokalt, hvorefter du vil kunne dele den. + Fedilab er stoppet :( + Du kan sende mig nedbrudsrapporten på e-mail. Det vil bidrage til problemløsningen :-)\n\nDu kan selv tilføje yderlige info. Mange tak! + Benyt wysiwyg + Når aktiveret, vil du let kunne formatere din tekst vha. værktøjerne. + Statistikker + Samlet status + Antal boosts + Antal favoritter + Antal omtaler + Antal følgere + Antal afstemninger + Antal svar + Antal af statusser + Statusser + Synlighed + Antal med medier + Antal med følsomme medier + Antal med CW + Første statusdato + Sidste statusdato + Første notifikationsdato + Sidste notifikationsdato + Frekvens + %s statusser pr. dag + %s notifikationer pr. dag + Datointerval + Grupper + Ingen grupper! + Deaktivér tilpassede animerede emojis + Diagrammer + Vis diagrammer + App\'en indsamler dine lokale data, vent venligst... + Sikkerhedskopi + Auto-sikkerhedskopiér statusser + Denne er en pr. konto mulighed. Den starter en tjeneste, der automatisk gemmer dine statusser lokalt i databasen. Dette muliggør statistikker og diagrammer + Auto-sikkerhedskopiér notifikationer + Dette er en pr. konto mulighed. Den starter en tjeneste, der automatisk gemmer dine notifikationer i den lokale database. Dette muliggør statistikker og diagrammer + Rapportér konto + Send en invitation + Dit instans tillader ikke registrering af en ny konto! + + %d stemme + %d stemmer + + + %d stemmeafgiver + %d stemmeafgivere + + + Enkeltvalg + Multivalg + + + 5 minutter + 30 minutter + 1 time + 6 timer + 1 dag + 3 dage + 7 dage + + + Webview + Direkte stream + + For deltagelse i min instans \"%1$s\" kan du downloade Fedilab:\n\nF-Droid: %2$s\nGoogle: %3$s\n\nÅbn derefter linket nedenfor med Fedilab og opret din konto :-)\n\n%4$s + + Din meningsmåling må ikke indeholde dubletvalg! + For alle konti + Database-cache + Ryd dit hjem tidslinje-cache + Ryd dine mellemlagrede statusser + Ryd dine bogmærker + Filer i cache + Notifikationer i alt + Skjul menupunkter + Fedilab kører live-notifikationer + For %1$s konti med %2$s begivenheder + Live-notifikationer for %1$s + Live-notifikationer bliver kun deaktiveret for denne konto. + Ryd cache ved afslutning + Cache (medier, cache-lagrede beskeder, data fra den indbyggede browser), vil automatisk blive ryddet, når appen forlader. + Stop med at følge denne konto? + Vis bekræftelsesdialog inden Følg stoppes + Udskift Youtube med Invidio.us + Invidious er en alternativ front-end til YouTube + Angiv din tilpassede vært eller udfykd ikke for at benytte invidio.os + Erstat Twitter med Nitter + Nitter er en alternativ open source Twitter front-end med fokus på fortrolighed. + Angiv din tilpassede vært eller udfykd ikke for at benytte nitter.net + Erstat Instagram med Bibliogram + Bibliogram er et open source Instagram front-end alternativt fokuseret på datafortrolighed. + Angiv din tilpassede vært eller lad stå tomt for at benytte bibliogram.art + Erstat Reddit med Libreddit + Libreddit er en fortrolighedorienteret, alternativ open-source Twitter front-end. + Angiv din tilpassede vært eller lad stå tomt for brug af libredd.it + Erstat Medium-links + Erstat medium.com-links med en open-source alternativ front-end med fokus på fortrolighed. + Standard: scribe.rip + Erstat Wikipedia-links + Erstat Wikipedia-link med en open-source alternativ front-end med fokus på fortrolighed. + Standard: wikiless.org + Skjul Fedilab-notifikationsbjælke + For at skjule den resterende notifikation på statusbjælken, så klik på øjeikonknappen og fjern derefter markeringen: \"Vises på statusbjælke\" + Brug et push-notifikationssystem for at modtage realtidsnotifikationer. + Ingen live-notifikationer + Live-notifikationer + Notifikationer hentes hvert 15. minut. + Tilføj notater + Notater til kontoen + Tillad for at komprimere store fotos til mindre størrelser med små eller ubetydelige kvalitetstab. + Tillad komprimering af videoer med bevarelse af deres kvalitet. + Appen komprimere medier, det kan være ganske længe… + Skift app-ikon + Tryk hér for at sikfte app-ikonet + Post nu + Indlæggets synlighed + Tryk hér for at tilføje fotos + Gyldige formater: Jpeg, png, gif\n\nMaks. filstørrelse: 15 MB\n\nAlbum kan indeholde op til 4 fotos eller videoer + Upload medier + Tilføj en valgfri billedtekst + Appen har modtaget en meget lang fejlmeddelelse fra API\'en %1$s + Beskedforhåndsvisning + Tilføj omtaler i hver besked + Henter samtale + Sortér efter + Videotitlen + Deltag i Peertube + Jeg er mindst 16 år gammel og accepterer %1$s for denne instans + Links + Skift farven på links (URL\'er, omtaler, tags mv.) i beskeder + Overskrift på genblogninger + Skift navnevisningsfarven øverst i beskeder + Skift brugernavnsfarven øverst i beskeder + Skift farven på overskriften på genblogninger + Indlæg + Baggrundsfarve på indlæg på tidslinjer + Nulstil farver + Tryk her for at nulstille alle de tilpassede farver + Nulstil + Ikoner + Farve på bundikoner på tidslinjer + Fastgør dette tag + Instansens logo + Redigér profil + Opret en handling + Oversættelse + Billedforhåndsvisning + Tekstfarve + Skift tekstfarve på kontainer + Effektuér ændringer + Appen skal genstartes for at effektuere ændringerne + Genstart + Benyt et tilpasset tema + Tillader farvetilsidesættelse for det ovenfor valgte tema + Tematisering + Gem inden + Temaet er eksporteret + Temaet er eksporteret til CSV + Anvend den primære farve på statusbjælken + Statusbjælkefarve + Gendan et standardtema + Importér et tema + Tryk hér for at importere et tema fra en tidligere eksport + Eksportér dette tema + Tryk hér for at eksportere det aktuelle tema + En fejl opstod under valg af temafilen + Temavælger + Vælg et præinstalleret tema + Temaer + Anvend primærfarven på navigeringsbjælken + Navigeringsbjælkefarve + Den underliggende farve for appens indhold. + Baggrundsfarve + Markeringsfarver vælger dele af UI\'en. + Markeringsfarve + Vist hyppigst i hele din app. + Primærfarve + Eksportér bogmærker til denne instans + Importér bogmærker fra denne instans + Brugerantal + Statusantal + Instansantal + Blokeret + Slutter om %s + Nyheder i %s + Du kan følge min konto for opdateringer + Denne instans er ikke tilgængelig på https://instances.social + Vis fuldt link + Del link + URL\'en er blevet kopieret til udklipsholderen + Åbn med en anden app + Tjek for omdirigering + Denne URL omdirigerer ikke + %1$s\n\nomdirigerer til\n\n%2$s + Skift brugeragenten + Angive en tilpasset brugeragent eller lad stå tom + Muliggør tilpasning af brugeragenten, der benyttes til API-kald eller med den indbyggede browser. + Fjern UTM-parametre + Appen fjerner automatisk UTM-parametre fra URL\'er, inden du følger et link. + Tendenser + Hot lige nu + %d folk omtaler + Twitter-konti (via Nitter) + Twitter-brugernavne, adskilt af mellemrum + Identitetsbeviser + Bekræftet identitet + Bekræftet af %1$s (%2$s) + Slet notifikationen + Vis flere muligheder + Det er en Pixelfed-historie + Upload et medie. Det føjes automatisk til din Pixelfed-historie. + Medie nu føjet til din historie! + Handling deaktiveret + Stop følgning + Noget gik galt. Tjek din download-mappe under Indstillinger. + Meddelelser + Ingen meddelelser! + Tilføj en reaktion + Benyt din favoritbrowser inde i appen. Fjern afkrydsningen af denne funktion for at åbne links eksternt. + Video-cache i MB, nul indikerer ingen cache. + Vandmærker + Tilføj automatisk et vandmærke i bunden af billeder. Teksten kan tilpasses for hver konto. + Ingen distributører fundet! + Du behøver en distributør for ar modtage push-notifikationer.\nDu finder flere oplysninger på %1$s.\n\nDu kan også deaktivere push-notifikationer i Indstillinger for at ignorere denne besked. + Vælg en distributør + diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml new file mode 100644 index 000000000..72527fa04 --- /dev/null +++ b/app/src/main/res/values-de/strings.xml @@ -0,0 +1,1130 @@ + + + Menü öffnen + Menü schließen + Über + Über die Instanz + Datenschutz + Zwischenspeicher + Abmelden + Anmelden + + Schließen + Ja + Nein + Abbrechen + Herunterladen + %1$s herunterladen + Datei gespeichert + Datei: %1$s + Passwort + E-Mail + Konten + Toots + Schlagwörter (tags) + Speichern + Wiederherstellen + Keine Ergebnisse. + Instanz + Instanz: mastodon.social + Konto %1$s wird verwendet + Konto hinzufügen + Der Inhalt des Toots wurde in die Zwischenablage kopiert + Die URL des Toots wurde in die Zwischenablage kopiert + Ändern + Wähle ein Bild… + Bereinigen + Kamera + Alles löschen + Toot übersetzen + Planen + Text- und Symbolgröße + Aktuelle Textgröße ändern: + Aktuelle Symbolgröße ändern: + Weiter + Zurück + Öffnen mit + Bestätigen + Medien + Teilen mit + Geteilt von Fedilab + Antworten + Benutzername + Entwürfe + Favoriten + Neue Folgende + Erwähnungen + Geteilte Beiträge + Geteilte Beiträge anzeigen + Antworten anzeigen + Im Browser öffnen + Übersetzen + Bitte warte einige Sekunden, bevor du diese Aktion durchführst. + + Startseite + Lokale Zeitleiste + Öffentliche Zeitleiste + Optionen + Favoriten + Kommunikation + Stummgeschaltete Nutzer + Blockierte Nutzer + Benachrichtigungen + Folgeanfragen + Einstellungen + Konto löschen + Möchtest du das Konto %1$s wirklich aus der App entfernen? + E-Mail senden + Pfad anklicken, um ihn zu ändern + Fehlgeschlagen! + Geplante Toots + Die folgenden Informationen könnten das Profil des Nutzers unvollständig wiedergeben. + Emoji einfügen + Die App verfügt derzeit nicht über benutzerdefinierte Emojis. + Push Benachrichtigungen + Sind Sie sicher, dass Sie sich ausloggen wollen? + Sind Sie sicher, dass Sie @%1$s@%2$s abmelden möchten? + + Kein Toot zum Anzeigen + Keine anzuzeigenden Elemente + Stories + Geteilt von %1$s + Diesen Toot deinen Favoriten hinzufügen? + Diesen Toot aus Ihren Favoriten entfernen? + Teile diesen Toot? + Geteilten Toot zurückziehen? + Diesen Toot anheften? + Diesen Toot nicht mehr anheften? + Stummschalten + Sperren + Melden + Entfernen + Kopieren + Teilen + Erwähnen + Zeitlich begrenzt lautlos schalten + Löschen & neu entwerfen + + Lautlosmodus für dieses Konto aktivieren? + Diesen Nutzer sperren? + Diesen Toot melden? + Diese Domäne sperren? + Lautlosmodus für dieses Konto aufheben? + Dieses Konto freigeben? + + + Benachrichtigen + Stumm + + + Diesen Toot entfernen? + Diesen Toot löschen & neu entwerfen? + + Lesezeichen + Zu Lesezeichen hinzufügen + Lesezeichen löschen + Keine Lesezeichen vorhanden + Beitrag wurde den Lesezeichen hinzugefügt! + Beitrag wurde aus Lesezeichen entfernt! + + %d s + %d m + %d h + %d d + + %d Sekunde + %d Sekunden + + + %d Minute + %d Minuten + + + %d Stunde + %d Stunden + + + %d Tag + %d Tage + + + Warnung + Was beschäftigt dich? + TOOT! + QUEET! + cw + Einen Toot verfassen + Auf einen Toot antworten + Schreib einen Queet + Auf einen Queet antworten + Wähle eine Mediendatei + Ein Fehler während der Auswahl ist aufgetreten! + Diese Datei löschen? + Dein Toot ist leer! + Sichtbarkeit des Toot + Standard-Sichtbarkeit der Toots: + Der Toot wurde gesendet! + Du antwortest auf diesen Toot: + Vertrauliche Inhalte? + + In öffentliche Zeitleisten eintragen + Nicht in öffentliche Zeitleisten eintragen + Nur an Folgende senden + Nur für erwähnte Nutzer freigeben + + Keine Entwürfe vorhanden! + Toot auswählen + Konto auswählen + Konten auswählen + Entwurf entfernen? + Auf die Schaltfläche klicken, um das ursprüngliche Toot anzuzeigen + Für Menschen mit Sehbehinderung beschreiben + + Keine Beschreibung verfügbar! + + Version %1$s + Entwickler: + Lizenz: + GNU GPL V3 + Quellcode: + Toot-Übersetzung: + Instanzensuche: + Symbolgestalter: + + Unterhaltung + + Keinen Nutzer gefunden + Keine Anfragen zu Folgen + Toots \n %1$s + Folgt \n %1$s + Folgende \n %1$s + Angeheftet\n %d + Authorisieren + Ablehnen + + Keine geplanten Toots vorhanden! + Schreibe einen Toot und wähle Planen im Topmenü. + Geplanten Toot löschen? + Medien: %d + Der Toot wurde geplant! + Der geplante Termin muss nach dem aktuellen Zeitpunkt liegen! + Der Batteriesparmodus ist aktiviert! Einiges funktioniert möglicherweise nicht wie erwartet. + + Die Dauer für den Lautlosmodus sollte mehr als eine Minute betragen. + %1$s wurde bis %2$s lautlos geschaltet.\n Du kannst den Lautlosmodus für diesen Nutzer beenden, indem du sein/ihr Profil besuchst. + %1$s ist bis %2$s lautlos geschaltet.\n Hier antippen, um den Lautlosmodus zu beenden. + + Keine Benachrichtigungen + hat dich erwähnt + hat eine neue Nachricht geschrieben + hat deinen Beitrag geteilt + hat deinen Status favorisiert + folgt dir + fragte, dir zu folgen + + und eine weitere Benachrichtigung + und %d weitere Benachrichtigungen + + + %d gefällt mir + %d Gefällt mir + + Entferne Benachrichtigung? + Entferne alle Benachrichtigungen? + Die Benachrichtigung wurde gelöscht! + Alle Benachrichtigungen wurden gelöscht! + + Folgt + Folgende + Angeheftet + + Fehler beim Laden der Client ID! + Verbinden zur Instanzdomain fehlgeschlagen! + Keine Internetverbindung! + Konto wurde gesperrt! + Das Konto wurde entsperrt! + Nutzer wurde lautlos geschaltet! + Lautlosmodus für dieses Konto aufgehoben! + Du folgst dem Nutzer! + Du folgst dem Nutzer nicht mehr! + Der Toot wurde geteilt! + Der Toot wird nicht länger geteilt! + Der Toot wurde deinen Favoriten hinzugefügt! + Der Toot wurde aus deinen Favoriten entfernt! + Toot wurde gemeldet! + Toot wurde gelöscht! + Toot wurde angeheftet! + Toot wurde abgelöst! + Es ist ein Fehler aufgetreten! + Ein Fehler ist aufgetreten! Die Instanz hat keinen Autorisierungscode gesendet! + Der Name der Instanz scheint ungültig zu sein! + Ein Fehler ist während des Kontowechsels aufgetreten! + Ein Fehler ist während der Suche aufgetreten! + Das Profil wurde gespeichert! + Keine Aktion kann durchgeführt werden + Datei wurde gespeichert! + Während der Übersetzung ist ein Fehler aufgetreten! + Übersetzungen sind in den Einstellungen deaktiviert + Entwurf gespeichert! + Bist du sicher das diese Instanz die Anzahl an Zeichen unterstützt? Gewöhnlich liegt der Wert bei 500. + Sichtbarkeit der Toots für Nutzer %1$s wurde geändert + + Anzahl der Toots pro Ladevorgang + Immer + WLAN + Fragen + Lade Anhang + Lade Bilder + Mehr anzeigen… + Zeige weniger… + Sensibler Inhalt + GIF Avatare deaktivieren + Pfad: + Entwürfe automatisch speichern + Füge die URL bei Anhängen in Toots hinzu + mir jemand folgt + jemand meinen Beitrag teilt + jemand meinen Beitrag favorisiert + ich erwähnt werde + Benachrichtigen, sobald eine Umfrage beendet ist + Benachrichtigung über neue Beiträge aktivieren + Bestätigungsdialog vor dem Teilen anzeigen + Bestätigungsdialog vor dem Favorisieren anzeigen + Nur bei WLAN benachrichtigen + Benachrichtigen? + Leise Benachrichtigungen + NSFW Anzeigedauer (in Sekunden, 0 ≙ deaktiviert) + Timeout der Medienbeschreibung (Sekunden, 0 bedeutet aus) + Ändere Profil + Benutzerdefiniertes Teilen + Ihre benutzerdefinierte Freigabeadresse … + Über mich… + Konto sperren + Änderungen speichern + Wähle ein Profilbild + Vorschaubilder anpassen + Toots mit mehr als 500 Zeichen automatisch unterteilen + Du hast die Grenze von 160 Zeichen erreicht! + Du hast die Grenze von 30 Zeichen erreicht! + Zwischen + und + Uhrzeit muss größer sein als %1$s + Uhrzeit muss kleiner sein als %1$s + Anfangszeit + Ende + Benutze internen Browser + In-App öffnen mit externem Browser + Javascript aktivieren + Inhaltswarnungen (CWs) automatisch einblenden + Erlaube Cookies von Drittanbietern + Dein API-Schlüssel; für Yandex kannst du ihn leer lassen + + Dunkel + Hell + Schwarz + + LED Farbe auswählen + + Blau + Cyan + Magenta + Grün + Rot + Gelb + Weiß + + Folgen + Nicht mehr blockieren + Lautlos + Lautlosmodus beenden + Anfrage gesendet + folgt dir + Suche + Ersten Buchstaben bei Antworten groß schreiben + Bildgröße verändern + Videogröße ändern + + Push Benachrichtigungen + Bitte bestätige welche Push Benachrichtigungen du erhalten möchtest. Diese können später im Benachrichtungsreiter aktiviert oder deaktiviert werden. + + Speicher leeren + Es sind %1$s Daten im Speicher.\n + \n + Möchtest du diese löschen? + MB + Speicher wurde geleert! %1$s wurden freigeben + + Titel + Titel … + Beschreibung + Schlüsselwörter + Schlüsselwörter … + + Synchronisieren + Filter + Deine Toots + Deine Benachrichtigungen + Öffentlich + Nicht gelistet + Privat + Direkt + Ein paar Schlüsselwörter… + Medien anzeigen + Angeheftet anzeigen + Keine passenden Einträge gefunden! + Toots sichern fürn %1$s + %1$s neue Toots wurden importiert + %1$s neue Benachrichtigungen wurden importiert + + Datum absteigend + Datum aufsteigend + + + Nein + Nur + Beide + + Keine Toots in der Datenbank gefunden. Drücke oben auf den Synchronisierungsknopf um sie abzurufen. + + Aufgezeichnete Daten + Auf dem Gerät werden nur allgemeine Informationen aus den Konten gespeichert. +Diese Daten sind streng vertraulich und können nur von der Anwendung verwendet werden. +Durch das Löschen der Anwendung werden diese Daten sofort entfernt.\n +⚠ Anmeldedaten und Passwörter werden nie gespeichert. Sie werden nur bei einer sicheren Authentifizierung (TLS) mit einer Instanz verwendet. + Berechtigungen: + - ACCESS_NETWORK_STATE: Erkennen von bestehenden Verbindungen zu WIFI Netzwerken.\n + - INTERNET: Genutzt für Anfragen an eine Instanz.\n + - WRITE_EXTERNAL_STORAGE: Dateien speichern oder App auf SD Karte verschieben.\n + - READ_EXTERNAL_STORAGE: Dateianhänge für toots.\n + - BOOT_COMPLETED: Benachrichtigungsdienst starten.\n + - WAKE_LOCK: Benachrichtigungsdienst. + API Berechtigungen: + Lesen: Daten lesen.\n +• Schreiben: Beiträge veröffentlichen und Dateien hochladen.\n +• Folgen: Folgen, nicht mehr folgen, blockieren, nicht mehr blockieren.\n\n +⚠ Diese Aktionen werden nur auf Verlangen des Benutzers durchgeführt. + Tracking und Bibliotheken + Die Anwendung nutzt keine Trackingwergzeuge(Zielgruppenbestimmung, Fehlerberichte, etc.) und enthält keinerlei Werbung.\n\n + Die Nutzung von Bibliotheken ist minimiert:\n + -Glide: Medien verwalten\n + -Android-Job: Dienste verwalten\n + -PhotoView: Bilder verwalten\n + + Übersetzen von Toots + Die Anwendung bietet die Möglichkeit, Toots mit Hilfe des Gebietsschemas des Geräts und der Yandex-API zu übersetzen.\nYandex verfügt über eine eigene Datenschutzrichtlinie, die du hier finden kannst: https://yandex.ru/legal/confidential/?lang=en + Danke an: + Mit regulären Ausdrücken filtern + Suche + Löschen + Mehr Toots abrufen… + + Listen + Möchtest du diese Liste wirklich dauerhaft löschen? + Noch keine Einträge vorhanden. Wenn Mitglieder dieser Liste etwas veröffentlichen, dann wird es hier erscheinen. + Zur Liste hinzufügen + Liste hinzufügen + Liste löschen + Liste bearbeiten + Neuer Listentitel + Das Konto wurde zur Liste hinzugefügt! + Du hast noch keine Listen! + + %1$s wurde verschoben nach %2$s + Authentifizierung funktioniert nicht? + Hier sind einige Vorschläge die helfen könnten:\n\n +- Prüfe ob der Instanzname richtig geschrieben ist\n\n +- Prüfe ob die Instanz online ist\n\n +- Benutze den Link unten falls du Zweifaktor-Authentifizierung (2FA) nutzt (nachdem der Instanzname eingetragen wurde)\n\n +- Du kannst diesen Link auch ohne 2FA benutzen\n\n +- Erstelle ein issue bei Framagit falls es dennoch nicht funktioniert: https://framagit.org/tom79/fedilab/issues + Media wurde geladen. Hier klicken zum anzeigen. + Diese Aktion kann sehr lange dauern. Du wirst benachrichtigt wenn sie abgeschlossen wurde. + Läuft noch, bitte warten… + Beiträge exportieren + Beiträge exportieren für %1$s + %1$s Toots von %2$s wurden exportiert. + Etwas ist schief gelaufen beim exportieren von%1$s + Beim Exportieren von Daten ist ein Problem aufgetreten! + Beim Importieren von Daten ist ein Problem aufgetreten! + + Proxy + Proxy aktivieren? + Host + Port + Login + Passwort + Toot Details beim Teilen hinzufügen + Unterstütze die app auf Liberapay + Es gibt einen Fehler im regulären Ausdruck! + Es wurden keine Zeitleisten in dieser Instanz gefunden! + Diese Instanz löschen? + Übersetzen in + Folge Instanz + Du folgst dieser Instanz bereits! + Der Instanz wird gefolgt! + Partnerschaften + Information + Verstecke geteilte Beiträge von %s + Auf Profil hervorheben + Geteilte Beiträge von %s anzeigen + Nicht mehr im Profil hervorheben + Dieser Benutzer wird nun in deinem Profil hervorgehoben + Dieser Benutzer wird nicht mehr in deinem Profil hervorgehoben + Geteilte Beiträge werden nun angezeigt! + Geteilte Beiträge werden nicht angezeigt! + Direktnachrichten + Filter + Keine Filter vorhanden. Du kannst durch Klicken auf \"+\" neue Filter erstellen. + Schlagwort oder Phrase + Lokale Zeitleiste + Öffentliche Zeitleiste + Benachrichtigungen + Unterhaltungen + Wird unabhängig vom umgebenen Text oder Inhaltswarnung eines Beitrags verglichen + Entfernen anstatt zu verstecken + Gefilterte Beiträge werden unwiderruflich gefiltert, selbst wenn der Filter später entfernt wurde + Wenn das Schlüsselwort oder -phrase nur Buchstaben und Zahlen enthält, wird es nur angewendet werden, wenn es dem ganzen Wort entspricht + Ganzes Wort + Kontext filtern + Ein oder mehrere Aspekte, wo der Filter greifen soll + Verfällt nach + Filter löschen? + Filter aktualisieren + Filter erstellen + Folgen + Momentan sind keine Konten vorhanden! + Folgen + Alle auswählen + Alle abwählen + %s wird gefolgt! + Erstelle Liste %s + Füge Konten der Liste hinzu + Konten wurden der Liste hinzugefügt + Füge Konten der Liste hinzu + Du hast noch keine Liste erstellt. Drücke auf den \"+\" Knopf um eine anzulegen. + Folgen + Trunk API + Dem Konto oder den Konten kann nicht gefolgt werden + Abruf des Remote-Accounts + Versteckte Medien automatisch einblenden + Neuer Folgender + Neu geteilt + Neuer Favorit + Neue Erwähnung + Umfrage beendet + Neuer Toot + Toots Backup + Neue Beiträge + Medien Download + Ändere Ton für Benachrichtigungen + Klingelton auswählen + Zeitfenster aktivieren + Video Anleitungen + Rufe entferntes Thema ab! + Keine blockierten Domänen! + Domäne nicht mehr blockieren + Möchtest du %s wirklich entsperren? + Möchtest du %s wirklich sperren?\n\nEs werden keine Inhalte aus dieser Domain in einer öffentlichen Zeitleiste oder in deinen Benachrichtigungen angezeigt. Deine Follower aus dieser Domain werden entfernt. + Blockierte Domänen + Blockiere Domäne + Die Domäne ist blockiert + Die Domäne wird nicht länger blockiert! + Rufe entfernten Status ab + Kommentar + Peertube Instanz + Kommentiere als Erster dieses Video mit den Knopf oben rechts! + %s mal gesehen + Dauer: %s + Instanz hinzufügen + Kommentare sind bei diesem Video nicht aktiviert! + Wähle eine Auflösung + Peertube Favoriten + Video wurde den Favoriten hinzugefügt! + Video wurde aus den Favoriten entfernt! + Keine Peertube Videos in den Favoriten! + Kanal + Videos + Kanäle + Emoji One verwenden + Information + Vorschau in allen Toots anzeigen + Neuer UX/UI-Designer + Video-Vorschau anzeigen + Konto-ID wurde in die Zwischenablage kopiert! + Sprache ändern + Standardsprache + Lange Toots kürzen + Kürze Toots mit mehr als \'x\' Zeilen. Null bedeutet deaktiviert. + Mehr anzeigen + Weniger anzeigen + Schlagwörter verwalten + Dieses Schlagwort existiert bereits! + Dieses Schlagwort wurde gespeichert! + Dieses Schlagwort wurde geändert! + Dieses Schlagwort wurde gelöscht! + Teilen des Beitrags planen + Teilen geplant! + Nichts geplant! + Teilen planen.]]> + Kunst-Zeitlinie + Öffne Menü + Zurück + Logo der Anwendung + Profilbild + Profilbanner + Kontaktiere den Administrator der Instanz + Hinzufügen + MastoHost Logo + Emoji Auswahl + Aktualisieren + Unterhaltung erweitern + Konto entfernen + Lösche die blockierte Domain + Benutzerdefinierte Emoji Auswahl + Video abspielen + Neuer Toot + Bild der Karte + Verstecke Media + Favicon + Medien zum Hinzufügen einer Beschreibung + + Nie + 30 Minuten + 1 Stunde + 6 Stunden + 12 Stunden + 1 Tag + 1 Woche + + In diesem Feld musst du deinen Instanz-Hostnamen eingeben.\nWenn du zum Beispiel ein Konto auf https://mastodon.social erstellt hast\nschreibe einfach mastodon.social (ohne https://)\n +Sobald du die ersten Buchstaben eintippst, werden Namensvorschläge angezeigt\n\n +⚠ Die Schaltfläche „Anmelden” funktioniert nur, wenn der Instanzname gültig und die Instanz erreuchbar ist! + Mehr Informationen + + Sprachen + Nur Medien + Heikle Inhalte anzeigen + Crowdin Übersetzungen + Crowdin Manager + Übersetzung der App + Über Crowdin + Bot + Pixelfed-Instanz + Mastodon-Instanz + Irgendwelche davon + Alle von denen + Keine von denen + Eines dieser Wörter (durch Leerzeichen getrennt) + Alle diese Wörter (durch Leerzeichen getrennt) + Wörter zum Filter hinzufügen (durch Leerzeichen getrennt) + Spaltenname ändern + Misskey Instanz + Auf Ihrem Gerät ist keine App installiert, die diesen Link unterstützt. + Abonnements + Übersicht + Angesagt + Kürzlich hinzugefügt + Lokal + Hochladen + Antworten + Kommentar entfernen + Möchtest du diesen Kommentar wirklich löschen? + Vollbild-Video + Modus für Videoaufnahmen + Datei zum Hochladen auswählen + Eigene Videos + Titel + Lizenz + Kategorie + Sprache + Dieses Video enthält heikle Inhalte + Kommentare zu Videos ermöglichen + Video aktualisieren + Beschreibung + Dieses Video wurde aktualisiert! + Hochladen abgebrochen! + Dieses Video wurde aktualisiert! + Wird hochgeladen, bitte warten … + Hier klicken, um die Videodaten zu bearbeiten. + Video löschen + Möchtest du dieses Video wirklich löschen? + Videos mit heiklen Inhalten anzeigen + Keine Videos zum Anzeigen! + Kommentar schreiben + Teilen + Zeitplanungsmodus wählen + Vom Gerät + Vom Server + Toots (Server) + Toots (Gerät) + Bearbeiten + Neugeladene Toots oberhalb der \"Mehr Toots abrufen\" Schaltfläche anzeigen + Zeitleisten + Benutzeroberfläche + Kontakte + %1$s kommentierte dein Video %2$s]]> + %1$s folgt deinem Kanal %2$s]]> + %1$s folgt deinem Konto]]> + %1$s wurde veröffentlicht]]> + %1$s wurde erfolgreich importiert]]> + %1$s ist fehlgeschlagen]]> + %1$s hat ein neues Video veröffentlicht: %2$s]]> + %1$s wurde zurückgezogen/gesperrt]]> + %1$s wurde aufgehoben]]> + Daten exportieren + Daten importieren + Zu importierende Datei auswählen + Bei der Auswahl der Sicherungsdatei ist ein Problem aufgetreten! + Einen öffentlichen Kommentar verfassen + Kommentar senden + Internetverbindung fehlgeschlagen. Ihre Nachricht wurde in Entwürfen gespeichert. + Reintext + HTML + Markdown + Konto abmelden + Alle + Die App unterstützen + Open Collective ermöglicht es Gruppen, schnell ein Kollektiv zu gründen, Mittel aufzubringen und transparent zu verwalten. + Link kopieren + Verbinden + Normal + Kompakt + Konsole + Anzeigemodus festlegen + Patch des Sicherheitsanbieters + Tracking-Domains aktualisieren + Die Tracking-Datenbank wurde aktualisiert! + Von der Anwendung blockierte HTTP-Aufrufe + Liste der blockierten Aufrufe + Senden + Die Datenbank wurde exportiert! + Empfohlene Hashtags + Zeitleiste mit Schlagwörtern filtern + Keine Schlagwörter + Schaltfläche „Mitteilungen löschen” im Tab „Mitteilungen” ausblenden + Metadaten abrufen, wenn die URL von anderen Apps geteilt wird + + Umfrage + Umfragen + Umfrage erstellen + Möglichkeit 1 + Möglichkeit 2 + %d auswählen + Du benötigst mindestens zwei Möglichkeiten für die Umfrage! + Fertig + endet um %s + Umfrage aktualisieren + Abstimmen + Eine Umfrage, in der du abgestimmt hast, ist beendet + Eine Ihrer Umfragen ist beendet + Anpassen + Kategorien + Zeitfenster + Erweitert + Ungelesene Toots als „Neu” kennzeichnen + Peertube + Zeitleiste verschieben + Zeitleiste ausblenden + Zeitleiste neu anordnen + Liste endgültig gelöscht + Gefolgte Instanz entfernt + Angeheftetes Schlagwort entfernt + Widerrufen + Es müssen zwei sichtbare Tabs vorhanden sein! + Zeitleisten neu ordnen + Hauptzeitleisten können nur ausgeblendet werden! + BBCode + Medien immer als sensibel kennzeichnen + GNU-Instanz + Gespeicherter Status + Schlagwörter in Antworten übernehmen + Lange Drücken, um Medien zu speichern + Sensible Medien unscharf darstellen + Zeitleisten in einer Liste anzeigen + Zeitleisten anzeigen + Toots von Bot-Konten kennzeichnen + Schlagwörter verwalten + Position innerhalb der Startleiste merken + Verlauf + Wiedergabelisten + Anzeigename + Sie haben noch keine Wiedergabelisten. Klicken Sie auf das Symbol „➕”, um eine neue Wiedergabeliste hinzuzufügen. + Du musst einen Anzeigenamen angeben! + Ein Kanal wird benötigt, wenn es sich um eine öffentliche Wiedergabeliste handelt. + Wiedergabeliste erstellen + Diese Wiedergabeliste ist leer. + Wiederherstellen + Galerie + Emoji + Sticker + Radierer + Text + Filter + Pinsel + Möchtest du den Editor wirklich verlassen, ohne die Grafik zu speichern? + Verwerfen + Speichern … + Grafik erfolgreich gespeichert! + Grafik konnte nicht gespeichert werden + Deckkraft + Fotobearbeitung aktivieren + Umfrageelement hinzufügen + Letztes Umfrageelement entfernen + Thema nicht mehr benachrichtigen + Thema erneut benachrichtigen + Nachrichten zu diesem Thema werden wieder benachrichtigt! + Nachrichten zu diesem Thema werden nicht mehr benachrichtigt + Anwendungseinstellungen öffnen + Zeitgesteuert lautlos schalten + Konto erwähnen + Zwischenspeicher aktualisieren + Status erwähnen + Neuigkeiten + Allgemein + Regional + Kunst + Journalismus + Aktivität + Spielend + Technologie + Nicht jugendfreier Inhalt + Pelzig + Nahrung + Logo der Instanz + Bei der Überprüfung der verfügbaren Instanzen ist ein Problem aufgetreten! + Mastodon beitreten + Instanz durch Anklicken einer Kategorie auswählen und dann auf eine Schaltfläche klicken. + Wähle eine Instanz aus, indem du auf eine Check-Schaltfläche tippst. + %1$s Nutzer + Passwort bestätigen + Ich akzeptiere %1$s und %2$s + Server-Regeln + Nutzungsbedingungen + Registrieren + Diese Instanz verwendet Einladungen. Ihr Konto muss von einem Administrator manuell genehmigt werden, bevor es verwendet werden kann. + Bitte alle Felder ausfüllen! + Die Passwörter stimmen nicht überein! + Ungültige E-Mail-Adresse! + Ihr Benutzername muss auf %1$s eindeutig sein + Du erhältst eine Bestätigungs-E-Mail + Mindestens 8 Zeichen verwenden + Das Passwort muss mindestens 8 Zeichen lang sein + Der Benutzername darf nur Buchstaben, Ziffern und Unterstriche enthalten + Konto erfolgreich erstellt. + Dein Konto wurde erfolgreich erstellt!\n\nDenke daran, deine E-Mail-Adresse innerhalb der nächsten 48 Stunden zu bestätigen.\n\nDu kannst nun dein Konto verbinden, indem du %1$s in das erste Feld schreibst und auf die Schaltfläche Verbinden klickst.\n\nWichtig: Wenn deine Instanz eine Bestätigung benötigt, erhältst du eine E-Mail, sobald sie geprüft wurde! + Möchtest du die Nachricht als Entwurf speichern? + Verwaltung + Berichte + Keine Berichte vorhanden! + Erneut mit Konto verbinden + Die Anwendung konnte nicht auf die Verwaltungsfunktionen zugreifen. Möglicherweise musst du das Konto erneut verbinden, um vollständigen Zugriff zu erhalten. + Ungelöst + Fernzugriff + Aktiv + Anstehend + Deaktiviert + Lautlos + Ausgesetzt + Berechtigungen + E-Mail-Status + Anmeldestatus + Beigetreten + Letzte IP-Adresse + Hinweis + Deaktivieren + Lautlos + Nutzer per E-Mail benachrichtigen + Benutzerdefinierte Warnung + Nutzer + Moderator + Administrator + Bestätigt + Nicht bestätigt + Gemeldete Status + Konto + Lautlosschaltung aufheben + Deaktivierung widerrufen + Unterbrechen + Unterbrechen widerrufen + Das Konto wurde lautlos geschaltet! + Lautlosmodus des Kontos wurde aufgehoben! + Das Konto wurde gesperrt! + Das Konto wurde entsperrt! + Das Konto wurde deaktiviert! + Das Konto wurde entsperrt! + Kontoinhaber wurde verwarnt! + Verwaltungsmenü anzeigen + Verwaltungsfunktion in Status anzeigen + Erlauben + Das Konto wurde freigegeben! + Das Konto wurde abgewiesen! + Mir zuordnen + Zuordnung aufheben + Als gelöst kennzeichnen + Als ungelöst kennzeichnen + Kein Inhalt! + Schaltfläche Fedilab-Funktionen anzeigen + Die Anwendung benötigt Zugriff auf die Audioaufzeichnung + Sprachnachricht + Schnelles Antworten aktivieren + Der Nutzer, dem du antwortest, sieht deine Nachricht möglicherweise nicht! + Wenn deaktiviert, lädt die App immer die letzten Status + Wenn deaktiviert, werden sensible Medien mit Hilfe einer Schaltfläche ausgeblendet + Medien in voller Größe speichern durch langes Antippen der Vorschaubilder + Füge oben rechts eine Dreipunkt-Schaltfläche […] hinzu, um alle Schlagwörter/Instanzen/Listen aufzulisten + Innerhalb des Zeitfensters sendet die App Mitteilungen. Du kannst dieses Zeitfenster mit dem richtigen Drehwähler umkehren (d. h. leise). + Zeigt eine Fedilab-Schaltfläche unter dem Profilbild. Dies ermöglicht einen Schnellzugriff auf In-App-Funktionen. + Ermöglicht das direkte Antworten in Zeitachsen unterhalb des Status + Vorschauen in Zeitachsen werden nicht beschnitten + Ermöglicht die Wiedergabe eingebetteter Videos direkt in der Zeitleiste + Ermöglicht es, die Art und Weise umzukehren, wie Zustände gelesen werden, die nach dem Klicken auf die Schaltfläche „Mehr nachladen” angezeigt werden. + Diese Option ermöglicht die Unterstützung aktueller Verschlüsselungs-Suiten. Es ist nützlich für ältere Android-Geräte oder wenn du keine Verbindung zu deiner Instanz herstellen kannst. + Exklusiv für PeerTube-Videos. Aktiviere diesen Modus, wenn Videos nicht wiedergeben können. + Diese Schlagwörter ermöglichen es, den Status von Profilen zu filtern. Du musst das Kontextmenü verwenden, um diese anzuzeigen. + Automatisches Einfügen eines Zeilenumbruchs nach der Anweisung den ersten Buchstaben groß zu schreiben. + Ermöglicht es Inhaltserstellern, den Status ihrer RSS-Feeds zu teilen + Verfassen + Max. Anzahl Wiederholversuche zum Hochladen von Medien + Hier einen neuen Ordner erstellen + Ordnernamen eingeben + Bitte einen gültigen Ordnernamen eingeben + Dieser Ordner existiert bereits.\nBitte einen anderen Ordnernamen eingeben. + Auswählen + Standardordner + Ordner + Ordner erstellen + Toast-Benachrichtigung anzeigen, nachdem eine Aktion abgeschlossen ist (boost, fav, usw)? + Lautlos geschaltete Instanzen wurden exportiert! + Instanz hinzufügen + Instanzen exportieren + Instanzen importieren + Absturzberichte + Absturzberichte aktivieren + Wenn aktiviert, wird lokal ein Absturzbericht erstellt. Anschließend kannst du diesen teilen. + Fedilab ist abgestürzt :( + Du kannst mir den Fehlerreport per E-Mail senden. Dies hilft mir bei der Fehlerbehebung:)\n\nDu kannst weitere Inhalte hinzufügen. Danke! + WYSIWYG-Funktionalität verwenden + Wenn aktiviert, kann Text ganz einfach mit Werkzeugen formatiert werden. + Statistik + Gesamtstatus + Anzahl geteilter Beiträge + Anzahl der Favoriten + Anzahl der Erwähnungen + Anzahl der Folgenden + Anzahl der Umfragen + Anzahl der Antworten + Status-Anzahl + Status + Sichtbarkeit + Anzahl mit Medien + Anzahl mit empfindlichen Medien + Anzahl mit CW + Datum des ersten Status + Datum des letzten Status + Erste Benachrichtigung + Letztes Benachrichtigungsdatum + Häufigkeit + %s Status pro Tag + %s Benachrichtigungen pro Tag + Datumsbereich + Gruppen + Keine Gruppen! + Eigene animierte Emojis deaktivieren + Diagramme + Diagramme anzeigen + Die Anwendung sammelt Ihre lokalen Daten, bitte warten … + Backup + Status automatisch als Backup sichern + Diese Option ist pro Konto. Es wird einen Dienst starten, der deine Status automatisch in einer lokalen Datenbank speichert. Dies erlaubt dir, Statistiken und Diagramme zu erhalten + Auto-Backup-Benachrichtigungen + Diese Option ist pro Konto. Es wird einen Dienst starten, der deine Benachrichtigungen automatisch lokal in der Datenbank speichert. Dies erlaubt Statistiken und Diagramme zu erhalten + Konto melden + Einladung senden + Deine Instanz erlaubt es nicht, ein neues Konto zu registrieren! + + %d Stimme + %d Stimmen + + + %d Stimme + %d Stimmen + + + Einfachauswahl + Mehrfachauswahl + + + 5 Minuten + 30 Minuten + 1 Stunde + 6 Stunden + 1 Tag + 3 Tage + 7 Tage + + + Torrent + Webansicht + + Um meiner Instanz \"%1$s\" beizutreten, kannstu du Fedilab herunterladen:\n\nF-Droid: %2$s\nGoogle: %3$s\n\nDann öffne den Link unten mit Fedilab und erstelle dein Konto :)\n\n%4$s + + Ihre Umfrage kann keine doppelten Optionen haben! + Für alle Konten + Datenbank-Cache + Lösche deinen Home Timeline Cache + Lösche deine gespeicherten Status + Lesezeichen löschen + Dateien im Cache + Gesamte Benachrichtigungen + Menüpunkte ausblenden + Fedilab läuft mit Echtzeitbenachrichtigungen + Für %1$s Konten mit %2$s Ereignissen + Live-Benachrichtigungen für %1$s + Live-Benachrichtigungen werden für dieses Konto aktiviert. + Cache beim Verlassen löschen + Der Cache (Medien, zwischengespeicherte Nachrichten, Daten aus dem eingebauten Browser) wird automatisch gelöscht, wenn die Anwendung verlassen wird. + Möchtest du diesem Konto nicht mehr folgen? + Bestätigungsdialog anzeigen, bevor nicht mehr gefolgt wird + Youtube durch Invidio.us ersetzen + Invidious ist ein alternatives Frontend zu YouTube + Geben Sie Ihren eigenen Host ein oder lassen Sie leer, um invidio.us zu verwenden + Twitter mit Nitter ersetzen + Nitter ist eine Open Source Alternative zum Twitter Front-End, mit dem Fokus auf Privatsphäre. + Geben Sie Ihren eigenen Host ein oder lassen Sie leer, um nitter.net zu verwenden + Instagram durch Bibliogram ersetzen + Bibliogram ist ein alternatives Open-Source-Frontend von Instagram, das sich auf den Datenschutz konzentriert. + Geben Sie Ihren benutzerdefinierten Host ein oder lassen Sie das Feld für die Verwendung von bibliogram.art frei + Reddit durch Libreddit ersetzen + Libreddit ist eine Open-Source-Alternative zum Reddit Front-End, das sich auf die Privatsphäre konzentriert. + Geben Sie Ihren eigenen Host ein oder lassen Sie leer, um libredd.it zu verwenden + Replace Medium links + Replace medium.com links with an open source alternative front-end focused on privacy. + Default: scribe.rip + Replace Wikipedia links + Replace Wikipedia link with an open source alternative front-end focused on privacy. + Default: wikiless.org + Fedilab Benachrichtigungsleiste ausblenden + Um die verbleibende Benachrichtigung in der Statusleiste zu verstecken, klicken Sie auf die Augensymbol-Schaltfläche und deaktivieren Sie: \"In Statusleiste anzeigen\" + Verwenden Sie ein Push-Benachrichtigungssystem, um Benachrichtigungen in Echtzeit zu erhalten. + Keine Echtzeit-Benachrichtigungen + Live notifications + Benachrichtigungen werden alle 15 Minuten abgerufen. + Notizen hinzufügen + Notizen für das Konto + Erlauben es, große Fotos in kleinere Bilder mit sehr weniger oder vernachlässigbaren Verlust der Qualität des Bildes zu komprimieren. + Erlaube das Komprimieren von Videos während die Qualität erhalten bleibt. + Die App komprimiert die Medien, dies könnte länger dauern… + App-Symbol ändern + Tippen, um das App-Symbol zu ändern + Posten + Sichtbarkeit des Toot + Hier tippen, um Fotos hinzuzufügen + Akzeptierte Formate: jpeg, png, gif \n\nMax Dateigröße: 15 MB \n\nAlben können bis zu 4 Fotos oder Videos enthalten + Medien Hochladen + Optionale Beschriftung hinzufügen + Die app erhielt eine sehr lange Fehlermeldung von der API %1$s + Nachrichtenvorschau + Erwähnungen in jeder Nachricht hinzufügen + Unterhaltung wird abgerufen + Sortieren nach + Titel für das Video + Peertube beitreten + Ich bin mindestens 16 Jahre alt und stimme den %1$s dieser Instanz zu + Links + Ändern Sie die Farbe der Links (URLs, Erwähnungen, Tags, etc.) in Nachrichten + Reblogs Titel + Ändern der Farbe des Anzeigenamens oben in den Nachrichten + Ändern der Farbe des Benutzernamens am Anfang von Nachrichten + Ändern der Farbe der Kopfzeile für Reblogs + Beiträge + Hintergrundfarbe der posts in den timelines + Farben zurücksetzen + Tippen Sie hier, um alle Ihre benutzerdefinierten Farben zurückzusetzen + Zurücksetzen + Symbole + Farbe der unteren Symbole in Zeitleisten + Diesen Tag anheften + Logo der Instanz + Profil bearbeiten + Aktion ausführen + Übersetzung + Bildvorschau + Textfarbe + Ändern Sie die Textfarbe in posts + Änderungen übernehmen + Sie müssen die Anwendung neu starten, um die Änderungen zu übernehmen + Neustarten + Verwenden Sie ein benutzerdefiniertes Design + Erlaubt es, die Farben des ausgewählten Themas oben zu überschreiben + Theming + Vorher speichern + Das Theme wurde exportiert + Das Design wurde erfolgreich als CSV exportiert + Die primäre Farbe auf die Statusleiste anwenden + Statusleistenfarbe + Standard-Theme wiederherstellen + Theme importieren + Hier tippen, um ein Theme aus einem vorherigen Export zu importieren + Exportieren des Designs + Hier tippen, um das aktuelle Theme zu exportieren + Bei der Auswahl der Designdatei ist ein Fehler aufgetreten + Theme-Auswahl + Vorinstalliertes Theme auswählen + Themes + Navigationsleiste mit der Primärfarbe einfärben + Farbe der Navigationsleiste + Die dem Inhalt der App zugrunde liegende Farbe. + Hintergrundfarbe + Akzente innerhalb der Oberfläche. + Akzentfarbe + Am häufigsten in Ihrer App angezeigt. + Primärfarbe + Lesezeichen zur Instanz exportieren + Lesezeichen von der Instanz importieren + Benutzeranzahl + Statusanzahl + Instanzanzahl + Blockiert + Endet in %s + Was ist neu in %s + Du kannst meinem Konto für Updates folgen + Diese Instanz ist auf https://instances.social nicht verfügbar + Vollständigen Link anzeigen + Link teilen + Der Link wurde in die Zwischenablage kopiert + Mit anderer App öffnen + Weiterleitung prüfen + Diese URL wird nicht weitergeleitet + %1$s \n\nweitergeleitet zu\n\n %2$s + User-Agent auswählen + Eigenen User-Agent setzen oder leer lassen + Ermöglicht es, den User-Agent für Api-Anrufe oder mit dem integrierten Browser anzupassen. + UTM-Parameter entfernen + Die App wird UTM-Parameter automatisch von URLs entfernen, bevor sie einen Link besucht. + Trends + Gerade im Trend + %d Leute sprechen + Twitter-Konten (über Nitter) + Twitter-Benutzernamen durch ein Leerzeichen getrennt + Identitätsnachweise + Verifizierte Identität + Verifiziert von %1$s (%2$s) + Benachrichtigung löschen + Weitere Optionen anzeigen + Es ist eine Pixelfed Geschichte + Laden Sie ein Medium hoch, es wird automatisch zu Ihrer Pixelfed Geschichte hinzugefügt. + Medien erfolgreich zu deiner Geschichte hinzugefügt! + Aktion deaktiviert + Entfolgen + Etwas ist schief gelaufen. Bitte überprüfen Sie Ihr Downloadverzeichnis in den Einstellungen. + Ankündigungen + Keine Ankündigungen! + Eine Reaktion hinzufügen + Verwenden Sie Ihren bevorzugten Browser innerhalb der App. Deaktiviere diese Funktion, um Links extern zu öffnen. + Video-Cache in MB, Null bedeutet keinen Cache. + Wasserzeichen + Automatisches Hinzufügen eines Wasserzeichens am unteren Rand von Bildern. Der Text kann für jedes Konto angepasst werden. + Keine Dienste gefunden! + Sie benötigen einen Dienst, um Push-Benachrichtigungen zu erhalten.\nWeitere Details finden Sie unter %1$s.\n\nSie können Push-Benachrichtigungen auch in den Einstellungen deaktivieren, um diese Nachricht zu ignorieren. + Dienstanbieter auswählen + diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml new file mode 100644 index 000000000..1272f03dc --- /dev/null +++ b/app/src/main/res/values-el/strings.xml @@ -0,0 +1,1142 @@ + + + Άνοιγμα μενού + Κλείσιμο μενού + Σχετικά με... + Σχετικά με την υπόσταση + Απόρρητο + Ενδιάμεση καταχώρηση + Αποσύνδεση + Σύνδεση + + Κλείσιμο + Ναι + Όχι + Ακύρωση + Λήψη + Λήψη του %1$s + Το υλικό αποθηκεύτηκε + Αρχείο: %1$s + Συνθηματικό + Email + Λογαριασμοί + Toots + Ετικέτες + Αποθήκευση + Επαναφορά + Κανένα αποτέλεσμα! + Υπόσταση + Υπόσταση: mastodon.social + Τώρα λειτουργεί με τον λογαριασμό %1$s + Προσθήκη λογαριασμού + Το περιεχόμενο του toot αντιγράφηκε ως απόκομμα + Η διεύθυνση URL της φωνής αντιγράφηκε στο πρόχειρο + Αλλαγή + Επιλογή φωτογραφίας… + Καθαρισμός + Φωτογραφική μηχανή + Σβήσε τα όλα + Μετέφρασε αυτό το toot. + Προγραμματισμός + Μέγεθος κειμένου και εικονιδίων + Άλλαξε το τρέχον μέγεθος κειμένου: + Άλλαξε το τρέχον μέγεθος εικονιδίων: + Επόμενο + Προηγούμενο + Άνοιξε με + Επαλήθευση + Υλικό + Μοιράσου με + Μοιράστηκε με το Fedilab + Απαντήσεις + Όνομα χρήστη + Προσχέδια + Αγαπημένα + Νέοι ακόλουθοι + Αναφορές + Boosts + Εμφάνισε τα boosts + Εμφάνισε τις αναφορές + Άνοιγμα στον περιηγητή + Μετάφραση + Παρακαλώ, πριν κάνετε αυτή την ενέργεια περιμένετε μερικά δευτερολεπτα. + + Αφετηρία + Τοπικό χρονολόγιο + Ομόσπονδο χρονολόγιο + Επιλογές + Αγαπημένα + Επικοινωνία + Χρήστες σε σίγαση + Χρήστες σε αποκλεισμό + Ειδοποιήσεις + Αιτήσεις ακολούθησης + Ρυθμίσεις + Διέγραψε έναν λογαριασμό + Διέγραψε τον λογαριασμό %1$s από την εφαρμογή; + Στείλε ένα email + Kλίκαρε στη διαδρομή για να την αλλάξεις + Απέτυχε! + Προγραμματισμένες φωνές + Οι παρακάτω πληροφορίες ίσως να είναι μια ανεπαρκής παρουσίαση του προσώπου του χρήστη. + Εισαγωγή ιδεογράμματος + Για την ώρα, η εφαρμογή δεν συνέλεξε προσαρμοσμένα ιδεογράμματα. + Push notifications + Are you sure you want to logout? + Are you sure you want to logout @%1$s@%2$s? + + Καμία φωνή προς εμφάνιση + No stories to display + Stories + Ενίσχυση από %1$s + Προσθήκη αυτής της φωνής στις αγαπημένες σου; + Απομάκρυνση αυτής της φωνής από τις αγαπημένες σου; + Δυνάμωμα αυτής της φωνής; + Χαμήλωμα αυτής της φωνής; + Καρφίτσωμα αυτής της φωνής; + Ξεκαρφίτσωμα αυτής της φωνής; + Σίγαση + Φραγή + Αναφορά + Απομάκρυνση + Αντιγραφή + Να γυρίζει + Μνημόνευση + Σίγαση με περιορισμένο χρόνο + Διαγραφή κ´ επανασχεδίαση + + Φίμωμα αυτού του λογαριασμού; + Φραγή αυτού του λογαριασμού; + Αναφορά αυτής της φωνής; + Φραγή αυτού του τομέα; + Ξεφίμωμα αυτού του λογαριασμού; + Unblock this account? + + + Ειδοποίηση + Σιωπηλό + + + Απομάκρυνση αυτής της φωνής; + Διαγραφή κ´ επανασχεδίαση αυτής της φωνής; + + Σελιδοδείκτες + Προσθήκη στους σελιδοδείκτες + Απομάκρυνση του σελιδοδείκτη + Δεν υπάρχουν σελιδοδείκτες προς εμφάνιση + Η κατάσταση προστέθηκε στους σελιδοδείκτες. + Η κατάσταση απομακρύνθηκε από τους σελιδοδείκτες. + + %d δευτ. + %d λεπ. + %d ώρ. + %d ημ. + + %d δευτερόλεπτο + %d δευτερόλεπτα + + + %d λεπτό + %d λεπτά + + + %d ώρα + %d ώρες + + + %d ημέρα + %d ημέρες + + + Προειδοποίηση + Τι συλλογιέσαι; + ΦΩΝΗ! + ΟΥ-ΟΥ! + Π.Π. + Γράψιμο μιας φωνής + Απάντηση σε μια φωνή + Γράψιμο ενός ού-ου + Απάντηση σε ένα ού-ου + Επιλογή ενός πολυμέσου + Παρουσιάστηκε κάποιο σφάλμα κατά την επιλογή του πολυμέσου. + Διαγραφή αυτού του πολυμέσου; + Η φωνή σου είναι άδεια. + Αναγνωσιμότητα της φωνής + Προώθηση των φωνών (προεπιλογή): + Η φωνή στάλθηκε! + Αποκρίνεσαι σε αυτή τη φωνή: + Ευαίσθητο περιεχόμενο; + + Δημοσίευση σε δημόσιες χρονοστήλες + Μη δημοσίευση σε δημόσιες χρονοστήλες + Δημοσίευση μόνο προς όσους σε παρακολουθούν + Δημοσίευση μόνο στους αναφερόμενους χρήστες + + Καθόλου προσχέδια. + Επέλεξε μια φωνή + Επέλεξε ένα λογαριασμό + Επέλεξε μερικούς λογαριασμούς + Απομάκρυνση του προσχέδιου; + Κλίκαρε στο πλήκτρο, για να εμφανιστεί η αρχική φωνή + Περιγραφή για όσους είναι με προβλήματα όρασης + + Δεν υπάρχει διαθέσιμη περιγραφή. + + Έκδοση %1$s + Προγραμματιστής: + Άδεια: + GNU Γενική Δημόσια Άδεια, Έκδοση 3η + Πηγαίος κώδικας: + Μετάφραση με... : + Αναζήτηση για υποστάσεις: + Σχεδιαστής εικονιδίου: + + Συζήτηση + + Δεν υπάρχει λογαριασμός προς εμφάνιση + Κανένα αίτημα παρακολούθησης + Φωνές \n %1$s + Παρακολουθεί \n %1$s + Τον παρακολουθούν \n %1$s + Πινέζες \n %d + Εξουσιοδότηση + Απόρριψη + + Δεν υπάρχει κάποια προγραμματισμένη φωνή για να εμφανιστεί. + Γράψε μία φωνή, και μετά, από τον κατάλογο των επιλογών, διάλεξε το Προγραμματισμός . + Διαγραφή των προγραμματισμένων φωνών; + Πολυμέσα: %d + Η φωνή προγραμματίστηκε. + Η προγραμματισμένη ημερομηνία θα πρέπει να είναι αργότερα από την τωρινή ώρα. + Είναι ενεργοποιημένη η εξοικονόμηση της ενέργειας. Ίσως να μη λειτουργήσει, όπως θα ήταν αναμενόμενο. + + Ο χρόνος φίμωσης θα πρέπει να είναι μεγαλύτερος από ένα λεπτό. + %1$s έχει φιμωθεί έως τις %2$s.\nΜπορείς να βγάλεις αυτό το λογαριασμό από τη σιγή, μέσω της σελίδας του προσώπου του ή της. + %1$s είναι φιμωμένος μέχρι τις %2$s.\nΚλίκαρε εδώ, για να τον βγάλεις από τη σιγή. + + Καμία ειδοποίηση προς εμφάνιση + σε ανέφερε + wrote a new message + ενίσχυσε την κατάστασή σου + έδειξε προτίμηση στην κατάστασή σου + άρχισε να σε παρακολουθεί + asked to follow you + + και ακόμη μία ειδοποίηση + και %d άλλες ειδοποιήσεις + + + %d, άρεσε + %d, άρεσε + + Διαγραφή μιας ειδοποίησης; + Διαγραφή όλων των ειδοποιήσεων; + Η ειδοποίηση διαγράφηκε. + Όλες οι ειδοποιήσεις διαγράφτηκαν. + + Παρακολουθείς + Σε παρακολουθούν + Καρφιτσωμένα + + Αδυναμία λήψης του αριθμού ταυτότητας (id) του πελάτη. + Αδυναμία σύνδεσης στον τομέα της υπόστασης. + Καμία σύνδεση στο διαδίκτυο. + Ο λογαριασμός φράχτηκε. + Ο λογαριασμός δεν είναι πλέον φραγμένος. + Ο λογαριασμός φιμώθηκε. + Ο λογαριασμός δεν είναι πλέον φιμωμένος. + Ο λογαριασμός παρακολουθείται. + Ο λογαριασμός πλέον δεν παρακολουθείται. + Η φωνή ενισχύθηκε. + Αυτή η φωνή δεν είναι ενισχυμένη, πλέον. + Η φωνή προστέθηκε στις αγαπημένες σου. + Η φωνή απομακρύνθηκε από τις αγαπημένες σου. + Έγινε αναφορά της φωνής. + Η φωνή διαγράφηκε. + Η φωνή καρφιτσώθηκε. + Η φωνή ξεκαρφιτσώθηκε. + Ώπα... Παρουσιάστηκε κάποιο σφάλμα. + Παρουσιάστηκε κάποιο σφάλμα. Η υπόσταση δεν απάντησε με κωδικό εξουσιοδότησης. + Ο τομέας της υπόστασης δεν φαίνεται για έγκυρος. + Παρουσιάστηκε κάποιο σφάλμα, κατά την εναλλαγή μεταξύ των λογαριασμών. + Παρουσιάστηκε κάποιο σφάλμα κατά την αναζήτηση. + Τα δεδομένα του προσώπου έχουν αποθηκευτεί. + Δεν μπορεί να εκτελεστεί καμία ενέργεια + Το πολυμέσο αποθηκεύθηκε. + Παρουσιάστηκε κάποιο σφάλμα κατά την μετάφραση. + Οι μεταφράσεις είναι απενεργοποιημένες στις ρυθμίσεις + Αποθηκεύτηκε το προσχέδιο. + Είσαι σίγουρος πως αυτή η υπόσταση επιτρέπει αυτό το πλήθος των χαρακτήρων; Συνήθως αυτή η τιμή είναι γύρω στους 500 χαρακτήρες. + Έχει αλλαχτεί η αναγνωσιμότητα των φωνών, για το λογαριασμό %1$s + + Πλήθος φωνών ανά φόρτωμα + Πάντα + με Γουάι-Φάι + Ερώτηση + Φόρτωση των πολυμέσων + Φόρτωση των εικόνων + Εμφάνιση περισσότερων… + Εμφάνιση λιγότερων… + Ευαίσθητο περιεχόμενο + Απενεργοποίηση των προσωπικών απεικονίσεων μορφής GIF + Διαδρομή: + Αυτόματη αποθήκευση των προσχεδίων + Προσθήκη της διεύθυνσης URL στις φωνές πολυμέσων + Ειδοποίηση όταν κάποιος σε παρακολουθεί + Ειδοποίηση όταν κάποιος ενισχύει την κατάστασή σου + Ειδοποίηση όταν σε κάποιον αρέσει η κατάστασή σου + Ειδοποίηση όταν κάποιος σε αναφέρει + Ειδοποίηση όταν τελειώνει μια ψηφοφορία + Notify for new posts + Προβολή του διαλόγου επιβεβαίωσης, πριν από την ενίσχυση + Προβολή του διαλόγου επιβεβαίωσης, πριν να γίνει προσθήκη στα Αγαπημένα + Ειδοποίηση μόνο όταν σε Γουάι-Φάι + Ειδοποιήσεις; + Άηχες Ειδοποιήσεις + Χρόνος λήξης της προβολής των Ακατάλληλων (σε δευτερόλεπτα· το 0 σημαίνει ανενεργό) + Χρόνος λήξης της Περιγραφής των Πολυμέσων (σε δευτερόλεπτα, το 0 σημαίνει ανενεργό) + Επεξεργασία του προσώπου + Προσαρμοσμένη κοινοποίηση + Η διεύθυνση URL… τς προσαρμοσμένης μοιρασιάς + Βιογραφικό… + Κλείδωμα του λογαριασμού + Αποθήκευση των αλλαγών + Επέλεξε μία εικόνα κεφαλίδας + Ταίριασμα των εικόνων προεπισκόπησης + Στις απαντήσεις, αυτόματο κομμάτιασμα των φωνών, που έχουν περισσότερους από 500 χαρακτήρες + Έχεις χρησιμοποιήσει το πλήθος των 160 επιτρεπόμενων χαρακτήρων. + Έχεις χρησιμοποιήσει το πλήθος των 30 επιτρεπόμενων χαρακτήρων. + Από + ως + Η ώρα θα πρέπει να είναι αργότερα από τις %1$s + Η ώρα θα πρέπει να είναι νωρίτερα από τις %1$s + Ώρα έναρξης + Ώρα λήξης + Χρήση του ενσωματωμένου πλοηγού διαδικτύου + Προσαρμοσμένες καρτέλες + Ενεργοποίηση των Σεναρίων της Τζάβα + Αυτόματη επέκταση της Προειδοποίησης Περιεχομένου + Να επιτρέπονται τα κουλουράκια τρίτων + Το API κλειδί σου· μπορείς να το αφήσεις κενό για το Yandex + + Σκοτεινό + Φωτεινό + Μαύρο + + Το χρώμα του λεντιού: + + Μπλε + Γαλάζιο + Μελιτζανί Ανοιχτό + Πράσινο + Κόκκινο + Κίτρινο + Λευκό + + Παρακολούθηση + Απόφραξη + Σίγαση + Ακύρωση της σίγασης + Η αίτηση στάλθηκε + Σε παρακολουθεί + Αναζήτηση + Το πρώτο γράμμα κεφαλαίο, στις απαντήσεις + Προσαρμογή των εικόνων + Αλλαγή διαστάσεων των βίντεων + + Ειδοποιήσεις προωθήσεων + Παρακαλώ, επιβεβαίωσε τις ειδοποιήσεις προωθήσεων, που θα ήθελες να λαμβάνεις. + Μπορείς να ενεργοποιήσεις ή να απενεργοποιήσεις αυτές τις ειδοποιήσεις, αργότερα, στις ρυθμίσεις (στην καρτέλα Ειδοποιήσεων). + + + Καθάρισμα της μικραποθήκευσης + Υπάρχουν %1$s δεδομένων στην μικραποθήκευση.\n\nΘα ήθελες να διαγραφούν; + Μεγαμπάιτια + Η μικραποθήκευση καθαρίστηκε. Απελευθερώθηκαν %1$s + + Τίτλος + Τίτλος… + Περιγραφή + Λέξεις-κλειδιά + Λέξεις-κλειδιά… + + Συγχρονισμός + Κόσκινο + Οι φωνές σου + Οι ειδοποιήσεις σου + Δημόσια + Αταξινόμητα + Ιδιωτικά + Απευθείας + Μερικές λέξει-κλειδιά… + Εμφάνιση των πολυμέσων + Εμφάνιση των καρφιτσωμένων + Δεν βρέθηκαν αποτελέσματα που να ταιριάζουν. + Δημιουργία αντιγράφου δεδομένων των φωνών του %1$s + Έχουν εισαχθεί %1$s νέες φωνές + Έχουν εισαχθεί %1$s νέες ειδοποιήσεις + + Ημερομηνίες προς τα πίσω + Ημερομηνίες προς τα εμπρός + + + Καθόλου + Μόνο + Καί + + Δεν βρέθηκαν φωνές στην βάση δεδομένων. Παρακαλώ, για να τις λάβεις, χρησιμοποίησε το πλήκτρο συγχρονισμού από τον κατάλογο των επιλογών. + + Αρχειοθετημένα δεδομένα + Στη συσκευή, αποθηκεύονται μόνο βασικές πληροφορίες των λογαριασμών. + Αυτά τα δεδομένα είναι αυστηρώς εμπιστευτικά, και μπορούν να χρησιμοποιηθούν μόνο από την εφαρμογή. + Η διαγραφή της εφαρμογής άμεσα απομακρύνει και αυτά τα δεδομένα.\n + ⚠ Ποτέ δεν αποθηκεύονται η σύνδεση και το συνθηματικό. Χρησιμοποιούνται μόνο κατά την ασφαλή ταυτοποίηση (SSL), με μία υπόσταση. + + Δικαιώματα: + - ACCESS_NETWORK_STATE: Used to detect if the device is connected to a WIFI network.\n + - INTERNET: Used for queries to an instance.\n + - WRITE_EXTERNAL_STORAGE: Used to store media or to move the app on a SD card.\n + - READ_EXTERNAL_STORAGE: Used to add media to toots.\n + - BOOT_COMPLETED: Used to start the notification service.\n + - WAKE_LOCK: Used during the notification service. + + Δικαιώματα API: + - Read: Ανάγνωση δεδομένων.\n + - Write: Δημοσίευση καταστάσεων και ανέβασμα πολυμέσων για καταστάσεις.\n + - Follow: Παρακολούθηση, κατάργηση παρακολούθησης, φραγή, απόφραξη.\n\n + ⚠ Αυτές οι ενέργειες εκτελούνται μόνο όταν τις ζητήσει ο χρήστης. + + Παρακολούθηση και Βιβλιοθήκες + Η εφαρμογή δεν χρησιμοποιεί εργαλεία ιχνηλάτησης (μετρήσεις κοινού, αναφορά σφαλμάτων, κλπ) και δεν περιέχει καμία διαφήμιση.\n\n + Η χρήση βιβλιοθηκών είναι περιορισμένη: \n + - Glide: Για τη διαχείριση πολυμέσων\n + - Android-Job: Για τη διαχείριση υπηρεσιών\n + - PhotoView: Για τη διαχείριση εικόνων\n + + Μετάφραση των φωνών + Η εφαρμογή προσφέρει τη δυνατότητα να μεταφράσεις φωνές χρησιμοποιώντας την τοπικοποίηση της συσκευής σου και την API του Yandex.\n + Το Yandex έχει κανονική πολιτική απορρήτου, την οποία μπορείς να βρεις εδώ: https://yandex.ru/legal/confidential/?lang=en + + Ευχαριστίες στους: + + Απομάκρυνση σύμφωνα με τις κανονικές εκφράσεις + Αναζήτηση + Διαγραφή + Λήψη περισσότερων φωνών… + + Κατάστιχα + Σίγουρα, να διαγραφεί αυτό το κατάστιχο, αμετάκλητα; + Ακόμη δεν υπάρχει τίποτα σε αυτό το κατάστιχο. Όταν κάποια μέλη αυτού του κατάστιχου θα δημοσιεύσουν νέες καταστάσεις, αυτές θα εμφανιστούν εδώ. + Προσθήκη στο κατάστιχο + Προσθήκη κατάστιχου + Διαγραφή κατάστιχου + Επεξεργασία κατάστιχου + Το όνομα του νέου κατάστιχου + Ο λογαριασμός προστέθηκε στο κατάστιχο. + Ακόμα να ορίσεις κάποια κατάστιχα. + + Το %1$s έχει πάει στο %2$s + Μήπως δεν δουλεύει η έλεγχος ταυτότητας; + Here are some checks that might help:\n\n + - Check there is no spelling mistakes in the instance name\n\n + - Check that your instance is not down\n\n + - If you use the two-factor authentication (2FA), please use the link at the bottom (once the instance name is filled)\n\n + - You can also use this link without using the 2FA\n\n + - If it still does not work, please raise an issue on Framagit at https://framagit.org/tom79/fedilab/issues + + Φορτώθηκε το πολυμέσο. Κλίκαρε εδώ για να προβληθεί. + Αυτή η ενέργεια μπορεί να είναι χρονοβόρα. Θα ειδοποιηθείς, όταν θα ολοκληρωθεί. + Ακόμη τρέχει· παρακαλώ, περίμενε… + Εξαγωγή των καταστάσεων + Εξαγωγή των καταστάσεων για %1$s + Εξάχθηκαν %1$s φωνές από τις %2$s . + Παρουσιάστηκε κάποιο σφάλμα, κατά την εξαγωγή δεδομένων για το %1$s + Παρουσιάστηκε κάποιο σφάλμα, κατά την εξαγωγή δεδομένων. + Παρουσιάστηκε κάποιο σφάλμα, κατά την εισαγωγή δεδομένων. + + Μεσολαβητής + Ενεργοποίηση του μεσολαβητή; + Οικοδεσπότης + Θύρα + Σύνδεση + Συνθηματικό + Προσθήκη των λεπτομερειών των φωνών, κατά το μοίρασμα + Υποστήριξη της εφαρμογής στο Liberapay + Υπάρχει κάποιο σφάλμα στην κανονική έκφραση. + Δεν βρέθηκε κάποια χρονοστήλη σε αυτή την υπόσταση. + Διαγραφή αυτής της υπόστασης; + Μετάφραση στο + Παρακολούθηση της υπόστασης + Ήδη παρακολουθείς αυτή την υπόσταση. + Η υπόσταση παρακολουθείται. + Συνεργασίες + Πληροφορίες + Απόκρυψη των ενισχύσεων από %s + Τοποθέτηση στο πρόσωπο + Προβολή των ενισχύσεων από %s + Μη τοποθέτηση στο πρόσωπο + Ο λογαριασμός έχει τοποθετηθεί, τώρα, σε προεξέχουσα θέση στο πρόσωπο + Ο λογαριασμός δεν είναι, πια, τοποθετημένος σε προεξέχουσα θέση στο πρόσωπο + Οι ανυψώσεις εμφανίζονται, τώρα. + Οι ενισχύσεις κρύβονται τώρα. + Απευθείας μήνυμα + Κόσκινα + Δεν υπάρχουν κόσκινα για προβολή. Για να δημιουργήσεις ένα, κλίκαρε στο κουμπί με το « + ». + Λέξη-κλειδί ή φράση + Σπιτική χρονοστήλη + Δημόσια χρονοστήλη + Ειδοποιήσεις + Συζητήσεις + Θα εφαρμοστεί, ασχέτως από μικρά ή κεφαλαία, καί στο κείμενο καί στην προειδοποίηση περιεχόμενου των φωνών + Πέταμα, αντί για απλό κρύψιμο + Οι κοσκινισμένες φωνές θα εξαφανιστούν, αμετάκλητα, ακόμη και αν, αργότερα, απομακρυνθεί αυτό το κόσκινο + Όταν η λέξη κλειδί είναι μοναχά αλφαριθμητική, θα έχει εφαρμογή μόνο αν ταιριάζει με ολόκληρη λέξη + Ολόκληρη λέξη + Προς κοσκίνισμα + Μία ή περισσότερες περιοχές, όπου θα εφαρμόζεται το κόσκινο + Λήγει μετά από + Διαγραφή του κόσκινου; + Ενημέρωση του κόσκινου + Δημιουργία κόσκινου + Ποιον να παρακολουθείς + Για την ώρα, δεν υπάρχουν καταχωρημένοι κατάλογοι. + Παρακολούθησε + Επιλογή όλων + Αποεπιλογή όλων + %s παρακολουθείται. + Δημιουργείται το κατάστιχο %s + Προστίθενται λογαριασμοί στο κατάστιχο + Λογαριασμοί προστέθηκαν στο κατάστιχο + Προστίθενται λογαριασμοί στο κατάστιχο + Ακόμη δεν έχεις δημιουργήσει ένα κατάστιχο. Για να δημιουργήσεις ένα νέο, κλίκαρε στο κουμπί με το « + ». + Ποιον να παρακολουθείς + Trunk API + Αδύνατη η παρακολούθηση του λογαριασμού ή των λογαριασμών + Λαμβάνεται απομακρυσμένος λογαριασμος + Αυτόματη επέκταση των κρυμμένων πολυμέσων + Νέα παρακολούθηση + Νέα Ενίσχυση + Νέο Αγαπημένο + Νέα Μνημόνευση + Η Ψηφοφορία Τελείωσε + Νέα Φωνή + Αντίγραφο των Φωνών + New posts + Κατέβασμα Πολυμέσων + Αλλαγή του ήχου των ειδοποιήσεων + Επιλογή Τόνου + Ενεργοποίηση της χρονικής περιόδου + Βίντεο Οδηγιών + Λαμβάνει την απομακρυσμένη πλοκή. + Κανένας φραγμένος τομέας. + Απόφραξη τομέα + Σίγουρα απόφραξη του %s; + Θέλεις, σίγουρα, να φράξεις το %s ;\n\nΔεν θα βλέπεις καθόλου περιεχόμενο από αυτόν τον τομέα, σε καμία δημόσια χρονοστήλη, ούτε στις ειδοποιήσεις σου. Όσοι σε παρακολουθούν, από αυτόν τον τομέα, θα απομακρυνθούν. + Φραγμένοι τομείς + Φράξιμο του τομέα + Ο τομέας έχει φραχτεί + Ο τομέας δεν είναι πλέον φραγμένος. + Λαμβάνει την απομακρυσμένη κατάσταση + Σχολιασμός + Υπόσταση Peertube + Μπορείς να προλάβεις να σχολιάσεις πρώτος αυτό το βίντεο, χρησιμοποιώντας το πλήκτρο, εκεί, πάνω δεξιά. + %s προβολές + Διάρκεια: %s + Προσθήκη μιας υπόστασης + Τα σχόλια δεν είναι ενεργοποιημένα σε αυτό το βίντεο. + Διάλεξε μια ανάλυση + Αγαπητά στο Peertube + Αυτό το βίντεο προστέθηκε στους σελιδοδείκτες. + Αυτό το βίντεο απομακρύνθηκε από τους σελιδοδείκτες. + Δεν υπάρχουν Πειρτιούμπια βίντεα στα αγαπημένα σου. + Κανάλι + Βίντεα + Κανάλια + Χρήση του Πρώτου Ιδεογράμματος + Πληροφορίες + Εμφάνιση των προεπισκοπήσεων σε όλες τις φωνές + Σχεδιαστής της νέας Εμπειρίας Χρήστη κ´ του νέου Περιβάλλοντος Χρήσης + Προβολή των προεπισκοπήσεων των βίντεων + Η ID ταυτότητα του λογαριασμού αντιγράφηκε στο πρόχειρο. + Αλλαγή της γλώσσας + Προεπιλεγμένη γλώσσα + Ψαλίδισμα των μακροσκελών φωνών + Ψαλίδισμα των φωνών με περισσότερες από «χ» γραμμές (το 0 σημαίνει απενεργοποιημένο). + Εμφάνιση περισσότερων + Εμφάνιση λιγότερων + Διαχείριση των καρτελών + Η ετικέτα υπάρχει, ήδη. + Η ετικέτα έχει αποθηκευθεί. + Η ετικέτα έχει αλλαχτεί. + Η ετικέτα έχει διαγραφεί. + Προγραμματισμός της ενίσχυσης + Η ενίσχυση προγραμματίστηκε. + Δεν υπάρχει κάποια προγραμματισμένη ενίσχυση για να εμφανιστεί. + Προγραμματισμό της ανύψωσης .]]> + Χρονοστήλη καλλιτεχνίας + Άνοιγμα του κατάλογου επιλογών + Επιστροφή + Το λογότυπο της εφαρμογής + Εικόνα του προσώπου + Σημαία του προσώπου + Επικοινωνία με τον διαχειριστή της υπόστασης + Προθήκη νέου + Λογότυπο του MastoHost + Επιλογέας ιδεογραμμάτων + Ανανέωση + Επέκταση της συνομιλίας + Απομάκρυνση ενός λογαριασμού + Διαγραφή του φραγμένου τομέα + Επιλογέας προσαρμοσμένων ιδεογραμμάτων + Αναπαραγωγή βίντεου + Νέα φωνή + Εικόνα της κάρτας + Απόκρυψη πολυμέσων + Έμβλημα + Προσθήκη περιγραφής των πολυμέσων (για όσους έχουν προβλήματα όρασης) + + Ποτέ + 30 λεπτά + 1 ώρα + 6 ώρες + 12 ώρες + 1 ημέρα + 1 εβδομάδα + + Σε αυτό το πεδίο, χρειάζεται να συμπληρώσεις το όνομα του οικοδεσπότη της υπόστασής σου.\nΓια παράδειγμα, αν δημιούργησες τον λογαριασμό σου στο https://mastodon.social\nΑπλά γράψε mastodon.social (χωρίς το https://)\n + Μπορείς να ξεκινήσεις να γράφεις τα πρώτα γράμματα, και θα προταθούν ολόκληρες οι ονομασίες.\n\n + ⚠ Το πλήκτρο Σύνδεσης λειτουργεί, μόνο όταν είναι έγκυρο το όνομα της υπόστασης, και είναι η υπόσταση σε λειτουργία! + + Περισσότερες πληροφορίες + + Γλώσσες + Μόνο πολυμέσα + Εμφάνιση των ακατάλληλων + Μεταφράσεις Crowdin + Διαχειριστής Crowdin + Μετάφραση της εφαρμογής + Σχετικά με το Crowdin + Αυτόματος Εργάτης + Υπόσταση Pixelfed + Υπόσταση Mastodon + Οποιαδήποτε από τις... + Όλες από τις... + Καμία από τις... + Με οποιαδήποτε από αυτές τις λέξεις (διαχωρισμένες με κενά) + Με όλες αυτές τις λέξεις (διαχωρισμένες με κενά) + Πρόσθεσε μερικές λέξεις για το κόσκινο (χωρισμένες με κενά) + Αλλαγή του ονόματος της στήλης + Υπόσταση Misskey + Δεν υπάρχει κάποια εφαρμογή εγκατεστημένη στο σύστημά σου, η οποία να υποστηρίζει αυτόν το δεσμό. + Συνδρομές + Σύνοψη + Τάσεις + Προστέθηκαν πρόσφατα + Τοπικά + Ανέβασμα + Απόκριση + Διαγραφή ενός σχόλιου + Θέλεις να διαγράψεις αυτό το σχόλιο, στα σίγουρα; + Βίντεο πλήρης οθόνης + Κατάσταση για τα βίντεα + Επέλεξε το αρχείο για αποστολή + Τα βίντεά μου + Τίτλος + Άδεια + Κατηγορία + Γλώσσα + Αυτό το βίντεο έχει περιεχόμενο ωμό ή ενηλίκων + Να επιτρέπονται τα σχόλια στο βίντεο + Ενημέρωση του βίντεου + Περιγραφή + Αυτό το βίντεο έχει ενημερωθεί. + Ακυρώθηκε η αποστολή. + Το βίντεο έχει ανεβεί. + Αποστέλνεται· υπομονή… + Κλίκαρε εδώ, για να επεξεργαστείς τα δεδομένα του βίντεου. + Διαγραφή του βίντεου + Να διαγραφεί το βίντεο, στα σίγουρα; + Προβολή των Ακατάλληλων βίντεων + Δεν υπάρχουν βίντεα για προβολή. + Γράψε ένα σχόλιο + Να γυρίζει + Επιλέξτε μια κατάσταση λειτουργίας προγραμματισμού + Από συσκευή + Από διακομιστή + Φωνές (Εξυπηρετητής) + Φωνές (Συσκευή) + Τροποποίηση + Εμφάνιση νέων φωνών πάνω από το πλήκτρο « Λήψη περισσότερων » + Χρονοστήλη + Περιβάλλον Χρήσης + Επαφές + %1$s σχολίασε το βίντεό σου %2$s]]> + %1$s παρακολουθεί το κανάλι σου %2$s]]> + %1$s παρακολουθεί το λογαριασμό σου]]> + %1$s]]> + %1$s]]> + %1$s]]> + %1$s Δημοσίευσε ένα νέο βίντεο: %2$s]]> + %1$s έχει μπει στη μαύρη λίστα]]> + %1$s έχει βγει στη μαύρη λίστα]]> + Εξαγωγή δεδομένων + Εισαγωγή δεδομένων + Επέλεξε το αρχείο προς εισαγωγή + Παρουσιάστηκε κάποιο σφάλμα στη συλλογή του αρχείου εξασφάλισης. + Προσθήκη ενός δημόσιου σχόλιου + Αποστολή σχόλιου + Δεν υπάρχει σύνδεση στο διαδίκτυο. Το μήνυμα σου έχει αποθηκευθεί στα προσχέδια. + Απλό κείμενο + HTML + Markdown + Αποσύνδεση του λογαριασμού + Όλα + Στήριξη της εφαρμογής + Η Ανοιχτή Συλλογικότητα δίνει, σε ομάδες, τη δυνατότητα να στήσουν γρήγορα μια συλλογικότητα, να συγκεντρώσουν έσοδα, και να τα διαχειριστούν με διαφάνεια. + Αντιγραφή του συνδέσμου + Σύνδεση + Κανονική + Συμπαγής + Κονσόλας + Όρισε την κατάσταση λειτουργίας της εμφάνισης + Χρήση του Προμηθευτή Ασφάλειας + Ενημέρωση των τομέων παρακολούθησης + Έχει γίνει ενημέρωση της βάσης δεδομένων παρακολούθησης. + Οι κλήσεις http είναι φραγμένες από την εφαρμογή + Κατάστιχο των φραγμένων κλήσεων + Υποβολή + Έγινε εξαγωγή της βάσης δεδομένων. + Προβεβλημένα #αριθμοθέματα + Κοσκίνισμα της χρονοστήλης με καρτέλες + Καμία καρτέλα + Απόκρυψη του πλήκτρου διαγραφής της ειδοποίησης, στην καρτέλα των ειδοποιήσεων + Προσκόλληση μιας εικόνας, όταν κοινοποιείται διεύθυνση URL + + Ψηφοφορία + Ψηφοφορίες + Δημιουργία ψηφοφορίας + 1η επιλογή + 2η επιλογή + Επιλογή %d + Απαιτούνται τουλάχιστον δύο επιλογές για μία ψηφοφορία. + Έγινε + τελειώνει στις %s + Ανανέωση της ψηφοφορίας + Ψήφισμα + Μία ψηφοφορία στην οποία συμμετείχες έχει τελειώσει + Μια ψηφοφορία που κάλεσες τελείωσε + Προσαρμογή + Κατηγορίες + Χρονική Περίοδος + Προχωρημένα + Εμφάνιση το σήματος «νέο», στις αδιάβαστες φωνές + Peertube + Μετακίνηση της χρονοστήλης + Απόκρυψη της χρονοστήλης + Αναδιάταξη των χρονοστήλων + Το κατάστιχο διαγράφηκε οριστικά + Η ακολουθούμενη υπόσταση απομακρύνθηκε + Η καρφιτσωμένη καρτέλα απομακρύνθηκε + Αναίρεση + Θα πρέπει να διατηρείς δύο καρτέλες ορατές. + Χρονοστήλες + Η κύρια χρονοστήλη μπορεί, μονάχα, να κρυφτεί. + BBCode + Πάντα σημείωση των πολυμέσων ως περιέχοντα ευαίσθητο περιεχόμενο + Υπόσταση GNU + Μικραποθηκευμένη κατάσταση + Προώθηση των καρτελών στις απαντήσεις + Διαρκές πάτημα για την αποθήκευση πολυμέσων + Θόλωση των ευαίσθητου περιεχόμενου πολυμέσων + Προβολή των χρονοστηλών σε ένα κατάστιχο + Προβολή των χρονοστηλών + Επισήμανση των αυτόματων εργατών στις φωνές + Διαχείριση των καρτελών + Απομνημόνευση της θέσης της Σπιτικής χρονοστήλης + Ιστορικό + Κατάστιχα αναπαραγωγής + Προβολή του ονόματος + Δεν έχεις κανένα κατάστιχο αναπαραγωγής. Κλίκαρε στο εικονίδιο « + », για να προσθέσεις ένα καινούριο κατάστιχο + Πρέπει να εισάγεις ένα όνομα ανάγνωσης + Το κανάλι απαιτείται, όταν το κατάστιχο αναπαραγωγής είναι δημόσιο. + Δημιουργία ενός κατάστιχου αναπαραγωγής + Δεν υπάρχει τίποτα στο κατάστιχο αναπαραγωγής, ακόμη. + επαναφορά + Εικονοθήκη + Ιδεόγραμμα + Αυτοκόλλητο + Σβήστρα + Κείμενο + Κόσκινο + Πινέλο + Έξοδος χωρίς αποθήκευση, εντελώς στα σίγουρα; + Απόρριψη + Αποθηκεύεται… + Η Εικόνα Αποθηκεύτηκε Επιτυχώς! + Αποτυχία αποθήκευσης της εικόνας + Αδιαφάνεια + Ενεργοποίηση του επεξεργαστή φωτογραφιών + Προσθήκη ενός αντικειμένου ψηφοφορίας + Απομάκρυνση του τελευταίου αντικειμένου ψηφοφορίας + Φίμωμα της συζήτησης + Ξεφίμωμα της συζήτησης + Η συζήτηση δεν είναι, πια, φιμωμένη! + Η συζήτηση είναι σιωπηρή + Άνοιγμα των χαρακτηριστικών της εφαρμογής + Χρονομετρημένη σίγαση + Μνημόνευση του λογαριασμού + Ανανέωση της μικραποθήκευσης + Μνημόνευση της κατάστασης + Νέα + Γενικά + Κατά περιοχή + Τέχνη + Δημοσιογραφία + Ακτιβισμός + Παιχνίδια + Τεχνολογία + Για ενήλικες + Furry + Φαΐ + Το λογότυπο της υπόστασης + Κάτι δεν πήγε καλά, κατά τον έλεγχο των διαθέσιμων υποστάσεων. + Συμμετοχή στο Μαστόδοντα + Διάλεξε μία υπόσταση, επιλέγοντας μία κατηγορία, και μετά κλίκαρε στο πλήκτρο ελέγχου. + Επέλεξε μια υπόσταση πατώντας στο πλήκτρο επιλογής. + %1$s χρήστες + Επιβεβαίωση του κωδικού πρόσβασης + Συμφωνώ με τους %1$s και τους %2$s + κανόνες του εξυπηρετητή + όρους παροχής υπηρεσιών + Εγγραφή + Η υπόσταση λειτουργεί με προσκλήσεις. Θα πρέπει, ο λογαριασμός σου να εγκριθεί χειροκίνητα, από έναν διαχειριστή, ώστε να μπορείς να τον χρησιμοποιήσεις. + Παρακαλώ, συμπλήρωσε όλα τα πεδία. + Οι κωδικοί πρόσβασης δεν ταιριάζουν. + Δεν φαίνεται να είναι έγκυρος αυτός ο λογαριασμός ηλεκτρονικού ταχυδρομείου . + Το ψευδώνυμό σου θα είναι μοναδικό στο %1$s + Θα σταλθεί, σε εσένα, ένα μήνυμα ηλεκτρονικού ταχυδρομείου, για επιβεβαίωση + Χρησιμοποίησε τουλάχιστον 8 χαρακτήρες + Ο κωδικός πρόσβασης θα πρέπει να αποτελείται από τουλάχιστον 8 χαρακτήρες + Το όνομα χρήστη επιτρέπεται να περιέχει μόνο γράμματα, αριθμούς, και κάτω παύλες + Ο λογαριασμός δημιουργήθηκε! + Your account has been created!\n\n + Think to validate your email within the 48 next hours.\n\n + You can now connect your account by writing %1$s in the first field and click on Connect.\n\n + Important: If your instance required validation, you will receive an email once it is validated! + + Αποθήκευση του μηνύματος στα προσχέδια; + Διαχείριση + Αναφορές + Δεν υπάρχουν αναφορές προς εμφάνιση. + Επανασύνδεση του λογαριασμού + The application failed to access the admin features. You might need to reconnect the account for having the correct scope. + Αδιευθέτητο + Απομακρυσμένο + Ενεργό + Εκκρεμές + Απενεργοποιημένο + Φιμωμένος + Σε αναστολή + Δικαιώματα + Κατάσταση ηλεκ. ταχυδρομείου + Κατάσταση σύνδεσης + Εγγράφηκε + Πρόσφατη IP + Προειδοποίηση + Απενεργοποίηση + Φίμωση + Notify the user per e-mail + Προσαρμοσμένη προειδοποίηση + Χρήστης + Συντονιστής + Διαχειριστής + Επιβεβαιωμένη + Μη επιβεβαιωμένη + Αναφερμένες καταστάσεις + Λογαριασμός + Αναίρεση της φίμωσης + Αναίρεση της απενεργοποίησης + Αναστολή + Αναίρεση της αναστολής + Ο λογαριασμός φιμώθηκε. + Ο λογαριασμός δεν είναι πλέον φιμωμένος. + Ο λογαριασμός έχει ανασταλεί. + Ο λογαριασμός δεν είναι πλέον σε αναστολή. + Ο λογαριασμός απενεργοποιήθηκε. + Ο λογαριασμός δεν είναι πλέον απενεργοποιημένος. + Ο λογαριασμός έχει προειδοποιηθεί. + Προβολή του κατάλογου επιλογών του διαχειριστή + Προβολή των λειτουργιών διαχείρισης στις καταστάσεις + Να επιτρέπεται + Ο λογαριασμός έχει εγκριθεί. + Ο λογαριασμός απορρίφθηκε. + Ανάθεση σε εμένα + Κατάργηση της ανάθεσης + Σήμανση ως λυμένο + Σήμανση ως άλυτο + Άδειο περιεχόμενο. + Προβολή του πλήκτρου λειτουργιών του Φέντιλαμπ + Η εφαρμογή πρέπει να έχει πρόσβαση στην εγγραφή ήχου + Ηχητικό μήνυμα + Ενεργοποίηση των γρήγορων απαντήσεων + Ο λογαριασμός, στον οποίο απαντάς, ίσως να μην μπορεί να δει το μήνυμά σου. + Αν απενεργοποιηθεί, η εφαρμογή θα φορτώνει, πάντα, τις τελευταίες καταστάσεις + Αν απενεργοποιηθεί, τα πολυμέσα με ευαίσθητο περιεχόμενο θα αποκρύπτονται με ένα πλήκτρο + Αποθήκευση των πολυμέσων, στις πλήρης διαστάσεις τους, με πάτημα διαρκείας στην προ-επισκόπηση + Προσθήκη ενός πλήκτρου αποσιωπητικών, πάνω δεξιά, για την παράθεση όλων των ετικετών/υποστάσεων/κατάστιχων + During the time slot, the app will send notifications. You can reverse (ie: silent) this time slot with the right spinner. + Προβολή ενός Φενδιλάμπιου πλήκτρου κάτω από την εικόνα του προσώπου. Είναι μια συντόμευση, η οποία επιτρέπει την πρόσβαση σε λειτουργίες της εφαρμογής. + Επιτρέπει την άμεση απόκριση, στις χρονοστήλες, κάτω από τις καταστάσεις + Οι προεπισκοπήσεις δεν θα περικόπτονται στις χρονοστήλες + Να επιτρέπεται η αναπαραγωγή βίντεων μέσα στις χρονοστήλες + Allow to reverse the way to read statuses that are displayed once tapping the fetch more button + This option allows to support recent cipher suites. It is useful for older Android devices or if you cannot connect to your instance. + Αποκλειστικά για βίντεα του Πειρτιούμπ: Κάνε εναλλαγή αυτής της λειτουργίας αν δεν αναπαράγονται σωστά. + These tags will allow to filter statuses from profiles. You will have to use the context menu for seeing them. + Αυτόματη εισαγωγή μίας νέας γραμμής, μετά από μια μνημόνευση, και μετατροπή του πρώτου γράμματος σε κεφαλαίο + Allow content creators to share statuses to their RSS feeds + Δημιουργία + Μέγιστο πλήθος προσπαθειών όταν ανεβαίνουν πολυμέσα + Δημιουργία, εδώ, ενός νέου Φάκελου + Εισαγωγή του ονόματος του φάκελου + Παρακαλώ, εισήγαγε ένα έγκυρο όνομα φακέλου + Αυτός ο φάκελος υπάρχει ήδη.\n Παρακαλώ δώσε ένα άλλο όνομα γι αυτό το φάκελο + Επιλογή + Προεπιλεγμένος Κατάλογος + Φάκελος + Δημιουργία φακέλου + Να προβάλλεται ένα φρυγαναίικο μήνυμα, μετά την ολοκλήρωση μίας ενέργειας (ενίσχυση, αγαπημένο, κλπ) ; + Οι φιμωμένες υποστάσεις εξάχθηκαν. + Προσθήκη μιας υπόστασης + Εξαγωγή των υποστάσεων + Εισαγωγή των υποστάσεων + Αναφορές κατάρρευσης + Ενεργοποίηση των αναφορών κατάρρευσης + Αν ενεργοποιηθεί, θα δημιουργηθεί, τοπικά, μια αναφορά κατάρρευσης και, μετά, θα μπορείς να τη μοιραστείς. + Το Φέντιλαμπ σταμάτησε :( + Μπορείς να μου στείλεις την αναφορά κατάρρευσης με ηλεκτρονικό ταχυδρομείο. Αυτό θα βοηθήσει στην βελτίωση του Φέντιλαμπ. :)\n\n Μπορείς να προσθέσεις και επιπλέον στοιχεία. Ευχαριστώ! + Χρήση του «Ό,τι Βλέπεις Παίρνεις» + Όταν ενεργοποιηθεί. θα μπορείς να μορφοποιήσεις εύκολα τα κείμενά σου χρησιμοποιώντας εργαλεία. + Στατιστικά + Σύνολο των καταστάσεων + Πλήθος ενισχύσεων + Πλήθος αγαπημένων + Πλήθος μνημονεύσεων + Πλήθος παρακολουθήσεων + Πλήθος ψηφοφοριών + Πλήθος απαντήσεων + Πλήθος καταστάσεων + Καταστάσεις + Αναγνωσιμότητα + Φέροντα πολυμέσα + Ευαίσθητου περιεχόμενου + Με Προειδοποίηση Περιεχόμενου + Πρώτη κατάσταση + Τελευταία κατάσταση + Ημερομηνία πρώτης ειδοποίησης + Τελευταία ειδοποίηση + Συχνότητα + %s καταστάσεις/ημέρα + %s ειδοποιήσεις/ημέρα + Ημερολογιακό Εύρος + Ομάδες + Χωρίς ομάδες. + Απενεργοποίηση των προσαρμοσμένων κινούμενων ιδεογραμμάτων + Διαγράμματα + Προβολή διαγραμμάτων + Η εφαρμογή συλλέγει τα τοπικά δεδομένα σου· λίγη υπομονή... + Αντίγραφο Ασφαλείας + Αυτόματη δημιουργία αντιγράφων ασφαλείας των καταστάσεων + Αυτή η επιλογή ισχύει ξεχωριστά για κάθε λογαριασμό. Θα εκκινήσει μια υπηρεσία, η οποία θα αποθηκεύει αυτόματα τις καταστάσεις σου, τοπικά, σε μία βάση δεδομένων. Αυτό θα επιτρέπει την λήψη στατιστικών και διαγραμμάτων. + Αυτόματη δημιουργία αντιγράφων ασφαλείας των ειδοποιήσεων + Αυτή η επιλογή ισχύει ξεχωριστά για κάθε λογαριασμό. Θα εκκινήσει μια υπηρεσία, η οποία θα αποθηκεύει αυτόματα τις ειδοποιήσεις σου, τοπικά, σε μία βάση δεδομένων. Αυτό θα επιτρέπει την λήψη στατιστικών και διαγραμμάτων. + Αναφορά του λογαριασμού + Αποστολή μίας πρόσκλησης + Η υπόσταση δεν επιτρέπει την καταχώρηση ενός νέου λογαριασμού. + + %d ψήφος + %d ψήφοι + + + %d ψηφοφόρος + %d ψηφοφόροι + + + Μονή επιλογή + Πολλαπλές επιλογές + + + 5 λεπτά + 30 λεπτά + 1 ώρα + 6 ώρες + 1 ημέρα + 3 ημέρες + 7 ημέρες + + + Webview + Άμεση ροή + + For joining my instance \"%1$s\", you can download Fedilab:\n\nF-Droid: %2$s\nGoogle: %3$s\n\nThen open the link below with Fedilab and create your account :)\n\n%4$s + + Δεν είναι δυνατό η ψηφοφορία σου να έχει διπλότυπες επιλογές. + Για όλους του λογαριασμούς + Μικραποθήκευση της βάσης δεδομένων + Καθάρισμα της μικραποθήκευσης την σπιτικής χρονοστήλης σου + Καθάρισμα των μικραποθηκευμένων καταστάσεών σου + Καθάρισμα των σελιδοδεικτών σου + Αρχεία στην μικραποθήκευσή σου + Σύνολο των ειδοποιήσεων + Απόκρυψη των στοιχείων του μενού + Το Φέντιλαμπ τρέχει ζωντανές ειδοποιήσεις + Για %1$s λογαριασμούς με %2$s γεγονότα + Ζωντανές ειδοποιήσεις για το %1$s + Οι ζωντανές ειδοποιήσεις θα είναι ενεργές γι αυτό το λογαριασμό. + Εκκαθάριση της μικραποθήκευσης κατά την έξοδο + Η προσωρινή μνήμη (πολυμέσα, μικραποθηκευμένα μηνύματα, δεδομένα από τον ενσωματωμένο πλοηγό) θα καθαρίζει αυτόματα, όταν θα κλείνεις την εφαρμογή. + Θέλεις να σταματήσεις να παρακολουθείς αυτό το λογαριασμό; + Προβολή ενός διαλόγου επιβεβαίωσης πριν την κατάργηση της παρακολούθησης + Αντικατάσταση του Youtube με το Invidio.us + Το Ινβίντιως είναι μία εναλλακτική διεπαφή για το Γιουτιούμπι + οικοδεσπότης (ή κενό για το invidio.us) + Αντικατάσταση του Twitter με το Nitter + Το Νίττερ είναι μία εναλλακτική, ανοιχτού κώδικα, διεπαφή για το Τουίτερ, η οποία δίνει έμφαση στο προσωπικό απόρρητο. + οικοδεσπότης (ή κενό για το nitter.net) + Replace Instagram with Bibliogram + Bibliogram is an open source alternative Instagram front-end focused on privacy. + Enter your custom host or leave blank for using bibliogram.art + Replace Reddit with Libreddit + Libreddit is an open source alternative Reddit front-end focused on privacy. + Enter your custom host or leave blank for using libredd.it + Replace Medium links + Replace medium.com links with an open source alternative front-end focused on privacy. + Default: scribe.rip + Replace Wikipedia links + Replace Wikipedia link with an open source alternative front-end focused on privacy. + Default: wikiless.org + Απόκρυψη της γραμμής ειδοποιήσεων του Φέντιλαμπ + Για απόκρυψη της υπόλοιπης ειδοποίησης στη γραμμή κατάστασης, πίεσε στο πλήκτρο με το μάτι, και μετά απο-επέλεξε την...: «Προβολή στη γραμμή κατάστασης» + Use a push notifications system for getting notifications in real time. + Καθόλου ζωντανές ειδοποιήσεις + Live notifications + Οι ειδοποιήσεις θα λαμβάνονται κάθε 15 λεπτά. + Προσθήκη σημειώσεων + Σημειώσεις για το λογαριασμό + Επιτρέπει την συμπίεση μεγάλων φωτογραφιών σε μικρότερων διαστάσεων φωτογραφίες, με πολύ μικρές, ή αμελητέες, απώλειες στην ποιότητα της εικόνας. + Επιτρέπει την συμπίεση βίντεων, διατηρώντας, όμως, την ποιότητά τους. + Η εφαρμογή συμπιέζει το πολυμέσο· αυτό μπορεί να διαρκέσει αρκετά… + Αλλαγή του εικονιδίου της εφαρμογής + Πίεσε για να αλλάξεις το εικονίδιο της εφαρμογής + Δημοσίευση + Αναγνωσιμότητα της δημοσίευσης + Πίεσε εδώ για προσθέσεις μία φωτογραφία + Αποδεκτές Μορφές Αρχείων: jpeg, png, gif \n\nΜέγιστο Μέγεθος Αρχείου: 15 MB \n\nΗ συλλογή μπορεί να περιέχει μέχρι 4 φωτογραφίες ή βίντεα + Ανέβασμα των πολυμέσων + Προσθήκη προαιρετικής περιγραφής + Η εφαρμογή έλαβε ένα μακροσκελές μήνυμα λάθους από την API %1$s + Προεπισκόπηση του μηνύματος + Προσθήκη μνημονεύσεων σε κάθε μήνυμα + Λαμβάνεται η συζήτηση + Ταξινόμηση κατά + Τίτλος του βίντεου + Συμμετοχή στο Peertube + Είμαι μεγαλύτερος από από 16 χρονών, και συμφωνώ με %1$s αυτής της υπόστασης + Σύνδεσμοι + Αλλαγή των χρωμάτων των συνδέσμων (URL, μνημονεύσεις, ετικέτες, κλπ) στα μηνύματα + Reblogs header + Αλλαγή του χρώματος, του προβαλλόμενου ονόματος, στην κορυφή των μηνυμάτων + Αλλαγή του χρώματος, του ονόματος χρήστη, στην κορυφή των μηνυμάτων + Change the color of the header for reblogs + Δημοσιεύσεις + Χρώμα παρασκηνίου των δημοσιεύσεων στις χρονοστήλες + Επαναφορά των χρωμάτων + Πίεσε εδώ, για να επαναφέρεις στις αρχικές τους τιμές όλα προσαρμοσμένα χρώματα που έχεις ορίσει + Επαναφορά + Εικονίδια + Χρώμα των κάτω εικονιδίων στις χρονοστήλες + Καρφίτσωμα αυτής της ετικέτας + Το λογότυπο της υπόστασης + Επεξεργασία του προσώπου + Επέλεξε μία κίνηση + Μετάφραση + Προεπισκόπηση εικόνας + Χρώμα κειμένου + Change the text color in pots + Εφαρμογή των αλλαγών + Θα πρέπει να επανεκκινήσεις την εφαρμογή για να εφαρμοστούν οι αλλαγές + Επανεκκίνηση + Χρήση ενός προσαρμοσμένου θέματος + Να επιτρέπεται η αντικατάσταση των χρωμάτων του παραπάνω επιλεγμένου θέματος + Θεματισμός + Πρώτα αποθήκευση + Έγινε εξαγωγή του θέματος + Έγινε επιτυχής εξαγωγή του θέματος σε μορφή CSV + Εφαρμογή του πρωτεύοντος χρώματος στη γραμμή κατάστασης + Χρώμα γραμμής κατάστασης + Αποκατάσταση ενός προεπιλεγμένου θέματος + Εισαγωγή ενός θέματος + Πίεσε εδώ, για να γίνει εισαγωγή ενός θέματος, από προηγούμενη εξαγωγή + Εξαγωγή του θέματος + Πίεσε εδώ για να εξάγεις το τωρινό θέμα + Παρουσιάστηκε κάποιο σφάλμα στην επιλογή του αρχείου με το θέμα + Theme Picker + Select a pre-installed theme + Themes + Apply the primary color to the navigation bar + Navigation bar color + The underlying color of the app’s content. + Background color + Accents select parts of the UI. + Accent color + Displayed most frequently across your app. + Primary color + Εξαγωγή των σελιδοδεικτών στην υπόσταση + Εισαγωγή των σελιδοδεικτών από την υπόσταση + User count + Status count + Instance count + Φραγμένοι + Να τελειώσει στις %s + Τι είναι νέο στο %s + Μπορείς να παρακολουθείς των λογαριασμό μου για ενημερώσεις + Αυτή η υπόσταση δεν είναι διαθέσιμη στην https://instances.social + Display full link + Share link + The URL has been copied to the clipboard + Open with another app + Check redirect + This URL does not redirect + %1$s \n\nredirects to\n\n %2$s + Change the user agent + Set a custom user agent or leave blank + Allows to customize the user agent used for api calls or with the built-in browser. + Remove UTM parameters + The app will automatically remove UTM parameters from URLs before visiting a link. + Trends + Trending now + %d people talking + Twitter accounts (via Nitter) + Twitter usernames space separated + Identity proofs + Verified identity + Verified by %1$s (%2$s) + Delete the notification + Display more options + It is a Pixelfed story + Upload a media, it will be automatically added to your Pixelfed story. + Media successfully added to your story! + Action disabled + Unfollow + Something went wrong, please check your download directory in settings. + Announcements + No announcements! + Add a reaction + Use your favourite browser inside the app. Uncheck this feature to open links externally. + Video cache in MB, zero means no cache. + Watermarks + Automatically add a watermark at the bottom of pictures. The text can be customized for each account. + No distributors found! + You need a distributor for receiving push notifications.\nYou will find more details at %1$s.\n\nYou can also disable push notifications in settings for ignoring that message. + Select a distributor + diff --git a/app/src/main/res/values-eo/strings.xml b/app/src/main/res/values-eo/strings.xml new file mode 100644 index 000000000..7bb007e86 --- /dev/null +++ b/app/src/main/res/values-eo/strings.xml @@ -0,0 +1,1142 @@ + + + Malfermi la menuo + Fermi la menuo + Pri + Pri la servilo + Privateco + Kaŝmemoro + Elsaluti + Ensaluti + + Fermi + Jes + Ne + Nuligi + Elŝuti + Elŝutante %1$s + Aŭdvidaĵo savita + Dosiero: %1$s + Pasvorto + Retpoŝto + Kontaj + Mesaĝoj + Etikedoj + Konservu + Restarigu + Neniu rezultoj! + Instanco + Instanco: mastodon.social + Nun laboras kun la konto %1$s + Aldoni konton + La enhavo de la hup estis kopiita al la tondujo + La URL de la hup estis kopiita al la tondujo + Ŝangi + Elekti bildon… + Pura + Fotilo + Forigu ĉiujn + Traduki tiun Mesaĝon. + Plano + Teksto kaj ikono grandecoj + Ŝanĝi la nunan tekstan grandecon: + Ŝanĝi la nunan ikonan grandecon: + Sekva + Antaŭa + Malfermi per + Validigu + Aŭdovidaĵoj + Konigi kun + Komuna tra Fedilab + Respondoj + Uzantnomo + Malnetoj + Stelumoj + Novaj sekvantoj + Mencioj + Diskonigoj + Montri diskonigojn + Montri la respondojn + Malfermu en retumilo + Traduku + Bonvolu, atendu kelkajn sekundojn antaŭ ol fari ĉi-tiun agon. + + Hejmo + Loka templinio + Frata templinio + Opcioj + Stelumoj + Komunikado + Silentigitaj uzantoj + Blokitaj uzantoj + Sciigoj + Petoj de sekvado + Agordoj + Forigi konton + Forigi la konton %1$s de la apliko? + Sendi per retmesaĝo + Tap on the path to change it + Fiaskis! + Planita hupoj + Subaj informoj povas nekomplete prezenti la profilon de la uzanto. + Enmeti emoĝion + The app did not collect custom emojis for the moment. + Push notifications + Are you sure you want to logout? + Are you sure you want to logout @%1$s@%2$s? + + Neniu hup por montri + No stories to display + Stories + Diskonigita de %1$s + Aldoni mesaĝon al vian stelumajn? + Remove this toot from your favourites? + Diskonigi ĉi-tiun mesaĝon? + Nuligi diskonigon? + Aplingi ĉi-tiu hup? + Malaplingi ĉi-tiu hup? + Silentigi + Bloki + Raporti + Forigi + Kopii + Konigi + Mencii + Timed mute + Forigi kaj reskribi + + Silentigi ĉi-tiun konton? + Bloki ĉi-tiun konton? + Report this toot? + Blokiti domajno? + Unmute this account? + Unblock this account? + + + Sciigi + Silenta + + + Forigi ĉi-tiun hup? + Delete & re-draft this toot? + + Legosignoj + Aldonu legosignon + Forigu legosignon + Neniu legosignojn por montri + Statuso estis aldonita al legosignoj! + Statuso estis forigita de legosignoj! + + %d s + %d m + %d h + %d d + + %d second + %d seconds + + + %d minute + %d minutes + + + %d hour + %d hours + + + %d day + %d days + + + Averto + Kion vi pensas? + HUP! + QUEET! + ea + Skribi mesaĝo + Respondi mesaĝon + Skribi queet + Respondi queet + Elekti aŭdovidaĵon + An error occurred while selecting the media! + Forigi ĉi tiun aŭdovidaĵon? + Via mesaĝo estas malplena! + Videbleco de la mesaĝo + Visibility of the toots by default: + La mesaĝo estis sendita! + You are replying to this toot: + Tikla enhavo? + + Afiŝi en publikaj tempolinioj + Ne afiŝi en publikaj tempolinioj + Afiŝi nur al sekvantoj + Afiŝi nur al menciitaj uzantoj + + Neniu malnetoj! + Elekti mesaĝo + Elekti kontojn + Elekti kelkaj kontoj + Forigi malneto? + Tap on the button to display the original toot + Describe for the visually impaired + + Neniu priskribo havebla! + + Release %1$s + Programistoj: + Permesilo: + GNU GPL V3 + Kodo: + Traduka de mesaĝoj: + Serĉi instencoj: + Icon designer: + + Konversacio + + Neniu konto por montri + Ne sekva peton + Mesaĝoj \n %1$s + Sekvante \n %1$s + Sekvante \n %1$s + Kovris \n %d + Akcepti + Malakcepti + + Neniu planitaj mesajoj por montri! + Skribi mesaĵo kaj tiam elekti Horaro de la pinta menuo. + Forigi planita mesaĝon? + Aŭdvidaĵo %d + La mesaĝo estis planita! + La planita dato devas pli granda ol la nunhoro! + Battery saver is enabled! It might not work as expected. + + The time for muting should be greater than one minute. + %1$s has been muted until %2$s.\n You can unmute this account from their profile page. + %1$s is muted until %2$s.\n Tap here to unmute the account. + + Neniun sciigon por montri + %s menciis vin + wrote a new message + {name} diskonigis vian mesaĝon + {name} stelumis vian mesaĝon + %s eksekvis vin + asked to follow you + + and another notification + and %d other notifications + + + %d ŝato + %d ŝatoj + + Forigi sciigo? + Forigi sciigoj? + The notification has been deleted! + All notifications have been deleted! + + Sekvantaj + Sekvitaj + Aplinglitaj + + Unable to get client id! + Unable to connect to instance domain! + Neniu interreta konekto! + La konto estis blokita! + La konto ne plu estas blokita! + La konto estis silentigita! + La konto ne plu silentigita! + La konto estis sekvita! + La konto estas ne plu sekvas! + The toot was boosted! + The toot is no longer boosted! + La hup estis aldonita al stelumoj! + The toot was removed from your favourites! + The toot was reported! + La mesaĝon forigitis! + La hup estis alpinglita! + La hup estis malalpinglita! + Ups ! Eraro okazis! + An error occurred! The instance did not return an authorisation code! + The instance domain does not seem to be valid! + An error occurred while switching between accounts! + An error occurred while searching! + La profila datumoj estis savita! + No action can be taken + La aŭdvidaĵo estis konservita! + Eraro okazis dum traduktas! + Translations are disabled in settings + Malneto saviĝis! + Are you sure this instance allows this number of characters? Usually, this value is close to 500 characters. + Visibility of the toots has been changed for the account %1$s + + Number of toots per load + Ĉiam + sendrata reto + Demandi + Laŭdu la aŭdvidaĵo + Ŝarĝi la bildojn + Montru pli… + Montru malpli… + Tikla enhavo + Disable GIF avatars + Vojo: + Save drafts automatically + Add URL of media in toots + Notify when someone follows you + Notify when someone boosts your status + Notify when someone favourites your status + Notify when someone mentions you + Notify when a poll ended + Notify for new posts + Show confirmation dialog before boosting + Show confirmation dialog before adding to favourites + Notify in WIFI only + Sciigi? + Silent Notifications + NSFW view timeout (seconds, 0 means off) + Media Description timeout (seconds, 0 means off) + Redakti profilon + Custom sharing + Your custom sharing URL… + Biografio… + Ŝlosi konton + Konservi ŝanĝojn + Choose a header picture + Fit preview images + Automatically split toots in replies when chars are over: + You have reached the 160 characters allowed! + You have reached the 30 characters allowed! + Inter + kaj + The time must be greater than %1$s + The time must be lower than %1$s + Komenca tempo + Malkomenca tempo + Use the built-in browser + Propra langetoj + Ebligi Ĝavoskripton + Automatically expand cw + Permesi tria-partiaj kuketojn + Your API key, you can leave blank for Yandex + + Malluma + Luma + Nigra + + Set LED colour: + + Blua + Cejana + Magenta + Verda + Ruĝa + Flava + Blanka + + Sekvu + Malbloku + Silentigu + Malsilentigu + Peto sendita + Sekvas vin + Serĉi + First letter in capital for replies + Resize pictures + Regrandigi videoj + + Puŝsciigoj + Please, confirm push notifications that you want to receive. + You can enable or disable these notifications later in settings (Notifications tab). + + + Vakigi Kaŝmemoron + There are %1$s of data in cache.\n\nWould you like to delete them? + Mo + Cache was cleared! %1$s were released + + Titolo + Titolo… + Priskribo + Ŝlosilvortoj + Ŝlosilvortoj… + + Sinkronigadi + Filtrili + Via mesaĝoj + Via sciigoj + Publika + Nelistigita + Privata + Rekta + Kelkaj ŝlosilvortoj… + Montri aŭdvidaĵoj + Montri aplingitaj + No matching result found! + Backup toots for %1$s + %1$s new toots have been imported + %1$s new notifications have been imported + + Datoj malsupreniranta + Datoj supreniranta + + + Ne + Nur + Ambaŭ + + No toots were found in database. Please, use the synchronize button from the menu to retrieve them. + + Registrita datumon + Only basic information from accounts are stored on the device. + These data are strictly confidential and can only be used by the application. + Deleting the application immediately removes these data.\n + ⚠ Login and passwords are never stored. They are only used during a secure authentication (SSL) with an instance. + + Permesoj: + - ACCESS_NETWORK_STATE: Used to detect if the device is connected to a WIFI network.\n + - INTERNET: Used for queries to an instance.\n + - WRITE_EXTERNAL_STORAGE: Used to store media or to move the app on a SD card.\n + - READ_EXTERNAL_STORAGE: Used to add media to toots.\n + - BOOT_COMPLETED: Used to start the notification service.\n + - WAKE_LOCK: Used during the notification service. + + API permesoj: + - Read: Read data.\n + - Write: Post statuses and upload media for statuses.\n + - Follow: Follow, unfollow, block, unblock.\n\n + ⚠ These actions are carried out only when user requests them. + + Tracking and Libraries + The application does not use tracking tools (audience measurement, error reporting, etc.) and does not contain any advertising.\n\n + The use of libraries is minimized: \n + - Glide: To manage media\n + - Android-Job: To manage services\n + - PhotoView: To manage images\n + + Translation of toots + The application offers the ability to translate toots using the locale of the device and the Yandex API.\n + Yandex has its proper privacy-policy which can be found here: https://yandex.ru/legal/confidential/?lang=en + + Dankon al: + + Filter out by regular expressions + Serĉi + Forigu + Fetch more toots… + + Listoj + Ĉu vi certas, ke vi volas porĉiame forigi ĉi tiun liston? + Ankoraŭ estas nenio en ĉi tiu listo. Kiam membroj de ĉi tiu listo afiŝos novajn mesaĝojn, ili aperos ĉi tie. + Aldoni al la listo + Aldoni al la listo + Forigi la liston + Redakti la liston + Nova listo titolo + La konto estis aldonita al la listo! + You don\'t have any lists yet! + + %1$s has moved to %2$s + Authentication does not work? + Here are some checks that might help:\n\n + - Check there is no spelling mistakes in the instance name\n\n + - Check that your instance is not down\n\n + - If you use the two-factor authentication (2FA), please use the link at the bottom (once the instance name is filled)\n\n + - You can also use this link without using the 2FA\n\n + - If it still does not work, please raise an issue on FramaGit at https://framagit.org/tom79/fedilab/issues + + Media has been loaded. Tap here to display it. + This action can be quite long. You will be notified when it will be finished. + Still running, please wait… + Eksporti statusoj + Export statuses for %1$s + %1$s toots out of %2$s have been exported. + Something went wrong when exporting data for %1$s + Something went wrong when exporting data! + Something went wrong when importing data! + + Prokurilo + Enable proxy? + Gastiganto + Pordo + Ensaluti + Pasvorto + Add toot details when sharing + Support the app on Liberapay + There is an error in the regular expression! + No timelines was found on this instance! + Delete this instance? + Traduki + Sekvi Instanco + You already follow this instance! + La instanco estas sekvita! + Partnerecoj + Informaĵo + Hide boosts from %s + Montri en profilo + Montri diskonigojn de %s + Ne montri en profilo + The account is now featured on profile + The account is no longer featured on profile + Boosts are now shown! + Boosts are now hidden! + Rekte mesaĝu + Filtriloj + No filters to display. You can create one by tapping on the \"+\" button. + Keyword or phrase + Hejma templinio + Publikaj templinioj + Sciigoj + Konversacioj + Will be matched regardless of casing in text or content warning of a toot + Drop instead of hide + Filtered toots will disappear irreversibly, even if filter is later removed + When the keyword or phrase is alphanumeric only, it will only be applied if it matches the whole word + Tuta vorto + Filtrilo kuntekstoj + One or multiple contexts where the filter should apply + Expire after + Forigi filtrilon? + Ĝisdatigo filtrilo + Krei filtrilon + Whom to follow + There is no accounts listed for the moment! + Sekvu + Elekti ĉion + Malelekti ĉion + %s estas sekvita! + Krei la listo %s + Aldoni kontojn al la listo + La kontoj estis aldonita al la listo + Aldoni kontojn al la listo + You have not created a list yet. Tap on the \"+\" button to add a new one. + Kiu sekvi + Trunk API + Konto(j) ne povas esti sekvita + Fetching remote account + Automatically expand hidden media + New follow + Nova Hup + Nova Steluma + Novaj mencioj + Voĉdonado Finiĝis + Nova Hup + Toots Backup + New posts + Media Download + Change notification sound + Elekti Tono + Enable time slot + How To Videos + Fetching remote thread! + No blocked domains! + Malbloki domajno + Are you sure to unblock %s? + Are you sure to block %s?\n\nYou will not see any content from that domain in any public timeline or in your notifications. Your followers from that domain will be removed. + Blokita domajnoj + Blokitu domajno + La domajno estas blokita + La domajno ne plu estas blokita! + Fetching remote status + Komentu + Peertube Instenco + Be the first to leave a comment on this video with the top right button! + %s vidoj + Daŭro: %s + Aldoni instencon + Comments are not enabled on this video! + Pick up a resolution + Peertube stelumoj + The video has been added to bookmarks! + The video has been removed from bookmarks! + There is no Peertube videos in your favourites! + Kanalo + Videoj + Kanaloj + Uzi Emoji One + Informaĵo + Display previews in all toots + New UX/UI designer + Display video previews + The account id has been copied in the clipboard! + Ŝanĝi lingvon + Defaŭlta lingvon + Truncate long toots + Truncate toots over \'x\' lines. Zero means disabled. + Montri pli + Montri malpli + Manage tags + The tag already exists! + The tag has been stored! + The tag has been changed! + The tag has been removed! + Schedule boost + The boost is scheduled! + No scheduled boost to display! + Schedule boost.]]> + Art timeline + Malfermu menuon + Reen + Logo of the application + Profilbildo + Profile banner + Contact admin of the instance + Aldoni novan + MastoHost logo + Emoji picker + Aktualigu + Expand the conversation + Forigi konton + Remove the blocked domain + Custom emoji picker + Play video + Nova Hup + Bildo de la karto + Kaŝu aŭdovidaĵojn + Piktogramo + Add description for media (for the visually impaired) + + Neniam + 30 minutoj + 1 horo + 6 horoj + 12 horoj + 1 tago + 1 semajno + + In this field, you need to write your instance host name.\nFor example, if you created your account on https://mastodon.social\nJust write mastodon.social (without https://)\n + You can start writing first letters and names will be suggested.\n\n + ⚠ The Login button will only work if the instance name is valid and the instance is up! + + More information + + Lingvoj + Nur aŭdvidaĵo + Montru NSFW + Crowdin tradukaj + Crowdin manager + Translation of the application + Pri Crowdin + Boto + Pixelfed instenco + Mastodono Instenco + Iu ajn de ĉi tiuj + Ĉiuj de ĉi tiuj + None of these + Any of these words (space-separated) + All these words (space-separated) + Add some words to filter (space-separated) + Change column name + Misskey instance + No app supporting this link is installed on your device. + Abonoj + Superrigardo + Aktualaĵoj + Ĵus aldonita + Loka + Alŝuti + Respondi + Forigi komenton + Are you sure to delete this comment? + Full screen video + Mode for videos + Select the file to upload + Mia videoj + Titolo + Permesilo + Kategorio + Lingvo + This video contains mature or explicit content + Enable video comments + Update video + Priskribo + The video has been updated! + Alŝuto nuligita! + The video has been uploaded! + Uploading, please wait… + Tap here to edit the video data. + Forigi video + Ĉu vi certas, ke vi volas forigi ĉi tiun filmeton? + Display NSFW videos + No videos to display! + Lasi komenton + Konigi + Choose a schedule mode + De aparato + De servilo + Hupoj (Servilo) + Hupoj (Aparato) + Ŝanĝi + Display new toots above the \"Fetch more\" button + Tempolinioj + Interfaco + Kontaktoj + %1$s commented your video %2$s]]> + %1$s is following your channel %2$s]]> + %1$s is following your account]]> + %1$s has been published]]> + %1$s succeeded]]> + %1$s failed]]> + %1$s published a new video: %2$s]]> + %1$s has been blacklisted]]> + %1$s has been unblacklisted]]> + Eksporti datumojn + Import Data + Elektu dosieron por importi + An error occurred when selecting the backup file! + Add a public comment + Sendi komenton + There is no Internet connection. Your message has been stored in drafts. + Plain text + HTML + Markdown + Ŝlosi konton + Ĉiuj + Subtenas la app + Open Collective enables groups to quickly set up a collective, raise funds and manage them transparently. + Kopii ligilon + Konekti + Normale + Kompakta + Konzolo + Set display mode + Patch the Security Provider + Update tracking domains + The tracking data base has been updated! + http calls blocked by the application + List of blocked calls + Sendi + The data base has been exported! + Featured hashtags + Filter timeline with tags + No tags + Hide the \"delete\" button in the notification tab + Attach an image when sharing a URL + + Baloto + Balotoj + Krei Baloto + Elekta 1 + Elekta 2 + Elekta %d + Vi bezonas du elektoj malpleje por la baloto! + Farite + end at %s + Refresh poll + Voĉdoni + Partoprenita balotenketo finiĝis + A poll you tooted has ended + Customize + Kategorioj + Time slot + Altnivela + Display \'new\' badge on unread toots + Peertube + Movi templinio + Kaŝi templinio + Reordigi templinioj + List permanently deleted + Followed instance removed + Pinned tag removed + Rezigni + You need to keep two visible tabs! + Reorder timelines + Main timelines can only be hidden! + BBCode + Ĉiam marki aŭdovidaĵojn tiklaj + GNU instenco + Cached status + Forward tags in replies + Long press to store media + Blur sensitive media + Display timelines in a list + Display timelines + Mark bot accounts in toots + Manage tags + Remember the position in Home timeline + History + Playlists + Montri nomon + You don\'t have any playlists. Tap on the \"+\" icon to add a new playlist + You must provide a display name! + The channel is required when the playlist is public. + Create a playlist + There is nothing in this playlist yet. + refari + Galerio + Emoĝio + Glumarko + Skrapileto + Teksto + Filtrilo + Brush + Are you sure you want to exit without saving the image? + Nuligi + Saving… + Image Saved Successfully! + Failed to save Image + Opakeco + Enable photo editor + Add a poll item + Remove last poll item + Mute conversation + Unmute conversation + The conversation is no longer muted! + The conversation is muted + Open application features + Timed mute + Mention the account + Refresh cache + Mention the status + News + Ĝeneralaj + Regiona + Arto + Ĵurnalismo + Aktivismo + Videoludado + Teknologio + Adult content + Felanaro + Manĝaĵo + Logo of the instance + Something went wrong when checking available instances! + Join Mastodon + Choose an instance by picking up a category, then tap on a check button. + Choose an instance by tapping on a check button. + %1$s users + Konfirmi la pasvorton + Mi konsentas %1$s kaj %2$s + server rules + Uzkondiĉojn + Enskribiĝi + This instance works with invitations. Your account will need to be manually approved by an administrator before being usable. + Please, fill all the fields! + Passwords don\'t match! + The email doesn\'t seem to be valid! + Your username will be unique on %1$s + You will be sent a confirmation e-mail + Use at least 8 characters + Password should contain at least 8 characters + Username should only contain letters, numbers and underscores + Account created! + Your account has been created!\n\n + Think to validate your email within the 48 next hours.\n\n + You can now connect your account by writing %1$s in the first field and click on Connect.\n\n + Important: If your instance required validation, you will receive an email once it is validated! + + Save the message in drafts? + Administrado + Raportoj + No reports to display! + Reconnect the account + The application failed to access the admin features. You might need to reconnect the account for having the correct scope. + Nesolvitaj + Remote + Active + Pending + Malŝaltita + Silenta + Paŭzita + Permesoj + Retpoŝto statuso + Login status + Membriĝis + Most recent IP + Averti + Malŝalti + Silentigi + Notify the user per e-mail + Custom warning + Uzanto + Kontrolanto + Administrado + Konfirmita + Ne konfirmita + Reported statuses + Konto + Malfari silentigi + Undo disable + Paŭziti + Malfari Paŭziti + La konto estas silentigita! + La konto ne plu silentigitas! + La konto paŭzitas! + La konto ne plu paŭzitas! + The account is disabled! + The account is no longer disabled! + The account has been warned! + Display the admin menu + Display the admin feature in statuses + Permesu + The account is approved! + The account is rejected! + Assign to me + Unassign + Mark as resolved + Mark as unresolved + Empty content! + Display Fedilab features button + The application needs to access audio recording + Voĉmesaĝo + Enable quick reply + The account you are replying might not see your message! + If disabled, the app will always load last statuses + If disabled, sensitive media will be hidden with a button + Store media in full size with a long press on previews + Add an ellipse button at the top right for listing all tags/instances/lists + During the time slot, the app will send notifications. You can reverse (ie: silent) this time slot with the right spinner. + Display a Fedilab button below profile picture. It is a shortcut for accessing in-app features. + Allow to reply directly in timelines below statuses + Previews will not be cropped in timelines + Allow to play embedded videos directly in timelines + Allow to reverse the way to read statuses that are displayed once tapping the fetch more button + This option allows to support recent cipher suites. It is useful for older Android devices or if you cannot connect to your instance. + Exclusively for Peertube videos. Switch this mode if you cannot play them. + These tags will allow to filter statuses from profiles. You will have to use the context menu for seeing them. + Automatically insert a line break after the mention to capitalize the first letter + Allow content creators to share statuses to their RSS feeds + Verki + Maximum retry times when uploading media + Create a new Folder here + Enter the folder name + Please enter a valid folder name + This folder already exists.\n Please provide another name for the folder + Elekti + Default Directory + Dosierujo + Krei dosierujon + Display a toast message after an action has been completed (boost, fav, etc.)? + Muted instances have been exported! + Aldoni instencon + Export instances + Import instances + Crash reports + Enable crash reports + If enabled, a crash report will be created locally and then you will be able to share it. + Fedilab haltis :( + You can send me by email the crash report. It will help to fix it :)\n\nYou can add additional content. Thank you! + Use the wysiwyg + When enabled, you will be able to format your text easily with tools. + Statistiko + Total statuses + Number of boosts + Nombro de stelumoj + Number of mentions + Number of follows + Number of polls + Number of replies + Number of statuses + Statuses + Visibility + Number with media + Number with sensitive media + Number with CW + First status date + Last status date + First notification date + Last notification date + Ofteco + %s statuses per day + %s notifications per day + Date range + Grupoj + No groups! + Disable custom animated emojis + Charts + Display charts + The application collects your local data, please wait… + Savkopii + Auto backup statuses + This option is per account. It will launch a service that will automatically store your statuses locally in the database. That allows to get statistics and charts + Auto backup notifications + This option is per account. It will launch a service that will automatically store your notifications locally in the database. That allows to get statistics and charts + Report account + Send an invitation + Your instance does not allow to register a new account! + + %d voĉdono + %d voĉdonoj + + + %d voter + %d voters + + + Single choice + Multiple choices + + + 5 minutoj + 30 minutoj + 1 horo + 6 horoj + 1 tago + 3 tagoj + 7 tagoj + + + Webview + Direct stream + + For joining my instance \"%1$s\", you can download Fedilab:\n\nF-Droid: %2$s\nGoogle: %3$s\n\nThen open the link below with Fedilab and create your account :)\n\n%4$s + + Your poll can\'t have duplicated options! + For all accounts + Database cache + Clear your home timeline cache + Clear your cached statuses + Clear your bookmarks + Files in cache + Total notifications + Hide menu items + Fedilab is running live notifications + For %1$s accounts with %2$s events + Live notifications for %1$s + Live notifications will be enabled for this account. + Clear cache when leaving + The cache (media, cached messages, data from the built-in browser) will be automatically cleared when leaving the application. + Do you want to unfollow this account? + Show confirmation dialog before unfollowing + Replace Youtube with Invidio.us + Invidious is an alternative front-end to YouTube + Enter your custom host or leave blank for using invidious.snopyta.org + Replace Twitter with Nitter + Nitter is an open source alternative Twitter front-end focused on privacy. + Enter your custom host or leave blank for using nitter.net + Replace Instagram with Bibliogram + Bibliogram is an open source alternative Instagram front-end focused on privacy. + Enter your custom host or leave blank for using bibliogram.art + Replace Reddit with Libreddit + Libreddit is an open source alternative Reddit front-end focused on privacy. + Enter your custom host or leave blank for using libredd.it + Replace Medium links + Replace medium.com links with an open source alternative front-end focused on privacy. + Default: scribe.rip + Replace Wikipedia links + Replace Wikipedia link with an open source alternative front-end focused on privacy. + Default: wikiless.org + Hide Fedilab notification bar + For hiding the remaining notification in the status bar, tap on the eye icon button then uncheck: \"Display in status bar\" + Use a push notifications system for getting notifications in real time. + No live notifications + Live notifications + Notifications will be fetched every 15 minutes. + Aldoni notojn + Notes for the account + Allow to compress large photos into smaller sized photos with very less or negligible loss in quality of the image. + Allow compressing videos while maintaining their quality. + The app is compressing the media, it can be quite long… + Change app icon + Tap to change the app icon + Afiŝo + Visibility of the post + Tap here to add photos + Accepted Formats: jpeg, png, gif \n\nMax File Size: 15 MB \n\nAlbums can contain up to 4 photos or videos + Upload media + Add an optional caption + The app received a very long error message from the API %1$s + Message preview + Add mentions in each message + Fetching conversation + Ordigi laŭ + Title for the video + Join Peertube + I am at least 16 years old and agree to the %1$s of this instance + Links + Change the color of links (URLs, mentions, tags, etc.) in messages + Reblogs header + Change the color of display name at the top of messages + Change the color of the user name at the top of messages + Change the color of the header for reblogs + Posts + Background color of posts in timelines + Reset colors + Tap here to reset all your custom colors + Reset + Icons + Color of bottom icons in timelines + Pin this tag + Logo of the instance + Edit profile + Make an action + Translation + Image preview + Text color + Change the text color in pots + Apply changes + You need to restart the application to apply changes + Restart + Use a custom theme + Allow to override colors of the selected theme above + Theming + Store before + The theme was exported + The theme has been successfully exported in CSV + Apply the primary color to the status bar + Status bar color + Restore a default theme + Import a theme + Tap here to import a theme from a previous export + Export the theme + Tap here to export the current theme + An error occurred when selecting the theme file + Theme Picker + Select a pre-installed theme + Themes + Apply the primary color to the navigation bar + Navigation bar color + The underlying color of the app’s content. + Background color + Accents select parts of the UI. + Accent color + Displayed most frequently across your app. + Primary color + Export bookmarks to the instance + Import bookmarks from the instance + User count + Status count + Instance count + Blocked + End in %s + What\'s new in %s + You can follow my account for updates + This instance is not available on https://instances.social + Display full link + Share link + The URL has been copied to the clipboard + Open with another app + Check redirect + This URL does not redirect + %1$s \n\nredirects to\n\n %2$s + Change the user agent + Set a custom user agent or leave blank + Allows to customize the user agent used for api calls or with the built-in browser. + Remove UTM parameters + The app will automatically remove UTM parameters from URLs before visiting a link. + Trends + Trending now + %d people talking + Twitter accounts (via Nitter) + Twitter usernames space separated + Identity proofs + Verified identity + Verified by %1$s (%2$s) + Delete the notification + Display more options + It is a Pixelfed story + Upload a media, it will be automatically added to your Pixelfed story. + Media successfully added to your story! + Action disabled + Unfollow + Something went wrong, please check your download directory in settings. + Announcements + No announcements! + Add a reaction + Use your favourite browser inside the app. Uncheck this feature to open links externally. + Video cache in MB, zero means no cache. + Watermarks + Automatically add a watermark at the bottom of pictures. The text can be customized for each account. + No distributors found! + You need a distributor for receiving push notifications.\nYou will find more details at %1$s.\n\nYou can also disable push notifications in settings for ignoring that message. + Select a distributor + diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml new file mode 100644 index 000000000..27f5e0452 --- /dev/null +++ b/app/src/main/res/values-es/strings.xml @@ -0,0 +1,1136 @@ + + + Abrir el menú + Cerrar menú + Acerca de + Sobre la instancia + Privacidad + Caché + Cerrar sesión + Iniciar sesión + + Cerrar + + No + Cancelar + Descargar + Descargando %1$s + Archivos multimedia guardados + Archivo: %1$s + Contraseña + Correo Electrónico + Cuentas + Toots + Etiquetas + Guardar + Restaurar + ¡No hay resultados! + Instancia + Instancia: mastodon.social + Ahora funciona con la cuenta %1$s + Agregar una cuenta + El contenido del toot ha sido copiado al portapapeles + La dirección URL del toot ha sido copiada al portapapeles + Cambiar + Seleccionar una imagen… + Borrar + Cámara + Eliminar todo + Traducir este toot. + Programar + Texto y tamaño de los íconos + Cambiar el tamaño del texto actual: + Cambiar el tamaño del ícono actual: + Siguiente + Anterior + Abrir con + Validar + Contenido multimedia + Compartir con + Compartido por Fedilab + Respuestas + Nombre de usuario + Borradores + Favoritos + Nuevos seguidores + Menciones + Impulsos + Mostrar impulsos + Mostrar respuestas + Abrir en navegador + Traducir + Por favor, espere unos segundos antes de realizar esta acción. + + Inicio + Cronología local + Timeline federado + Opciones + Favoritos + Comunicación + Usuarios silenciados + Usuarios bloqueados + Notificaciones + Solicitud de seguir + Configuración + Eliminar una cuenta + ¿Borrar la cuenta %1$s de la aplicación? + Envíar un correo + Pulsa en la dirección para cambiarla + ¡Falló! + Toots programados + La siguiente información puede mostrar el perfil del usuario incompleto. + Insertar Emoji + La aplicación no recogió emojis personalizados por el momento. + Notificaciones push + ¿Estás seguro de que quieres cerrar tu sesión? + ¿Está seguro de que quieres cerrar la sesión @%1$s@%2$s? + + Ningún toot para mostrar + No hay historias para mostrar + Historias + Retooteado por %1$s + ¿Agregar este toot a favoritos? + ¿Quitar este toot de tus favoritos? + ¿Impulsar este toot? + ¿Dejar de impulsar este toot? + ¿Fijar este toot? + ¿Dejar de fijar este toot? + Silenciar + Bloquear + Reportar + Borrar + Copiar + Compartir + Mencionar + Silenciado temporal + Borrar & redactar + + ¿Silenciar esta cuenta? + ¿Bloquear esta cuenta? + ¿Reportar este toot? + ¿Bloquear este dominio? + ¿Dejar de silenciar esta cuenta? + ¿Desbloquear esta cuenta? + + + Notificar + Silencioso + + + ¿Borrar este toot? + Borrar & redactar este Toot? + + Marcadores + Añadir a marcadores + Eliminar marcador + No hay marcadores para mostrar + ¡El estado ha sido añadido a marcadores! + ¡El estado fue eliminado de marcadores! + + %d s + %d m + %d h + %d d + + %d segundo + %d segundos + + + %d minuto + %d minutos + + + %d hora + %d horas + + + %d día + %d días + + + Advertencia + ¿En qué estás pensando? + ¡Toot! + QUEET! + cw + Escribe un toot + Responder a un toot + Escribir un queet + Responder a un queet + Selecciona un medio + ¡Un error ocurrió mientras seleccionaba un contenido multimedia! + ¿Eliminar este contenido multimedia? + ¡Tu toot está vacío! + Visibilidad del toot + Visibilidad de los toots por defecto: + ¡El toot ha sido enviado! + Tú estas respondiendo a este toot: + ¿Contenido sensible? + + Publicar en cronologías públicas + No publicar en timelines públicas + Publicar a seguidores solamente + Publicar a usuarios mencionados únicamente + + ¡No hay borrador! + Elegir un toot + Elegir una cuenta + Seleccionar alguna cuenta + ¿Eliminar borrador? + Pulsa en el botón para mostrar el toot original + Describir para los discapacitados visuales + + ¡No hay descripción disponible! + + Liberar %1$s + Desarrollador: + Licencia: + GNU GPL V3 + Código fuente: + Traducción de los toots: + Búsqueda de instancias: + Diseñador de ícono: + + Conversación + + Ninguna cuenta para mostrar + Ninguna solicitud de seguir + Toots \n %1$s + Siguiendo: \n %1$s + Seguidores \n %1$s + Fijar \n %d + Autorizar + Rechazar + + ¡Ningún toot programado para mostrar! + Escribe un toot y luego elige Programar desde menú superior. + ¿Borrar el toot programado? + Contenido multimedia: %d + ¡El toot ha sido programado! + ¡La fecha programada debe ser mayor que la hora actual! + ¡Ahorrador de batería activado! Podría no trabajar como lo esperado. + + El tiempo de silenciado debería ser mayor que un minuto. + %1$s ha sido silenciado hasta %2$s.\n Puedes anular el silenciado de esta cuenta desde su página de perfil. + %1$s está silenciado hasta %2$s.\n Pulsa aquí para anular el silenciado de la cuenta. + + Ninguna notificación para mostrar + te mencionó + escribió un nuevo mensaje + impulsó tu estado + marcó como favorito tu estado + te siguió + solicitó seguirte + + y otra notificación + y otra notificación + + + %d me gusta + %d me gusta + + ¿Eliminar una notificación? + ¿Eliminar todas las notificaciones? + ¡La notificación ha sido eliminada! + ¡Todas las notificaciones han sido eliminadas! + + Siguiendo + Seguidores + Fijado + + ¡No se puede obtener la identificación del cliente! + ¡No se puede conectar al dominio de la instancia! + ¡Sin conexión a Internet! + ¡La cuenta ha sido bloqueada! + ¡La cuenta ya no está bloqueada! + ¡La cuenta ha sido silenciada! + ¡La cuenta ya no esta silenciada! + ¡La cuenta fue seguida! + ¡La cuenta ya no esta seguida! + ¡El toot fue impulsado! + ¡El toot ya no está impulsado! + ¡El toot fue agregado a tus favoritos! + ¡El toot fue removido de tus favoritos! + ¡El toot fue reportado! + ¡El toot fue eliminado! + ¡Este toot fue fijado! + ¡Este toot ya no está fijado! + ¡Ups! ¡Un error ha ocurrido! + ¡Un error ha ocurrido! ¡La instancia no devolvió el código de autorización! + ¡El dominio de la instancia no parece estar validado! + ¡Un error ocurrió mientras cambiaba entre cuentas! + ¡Un error ocurrió mientras buscaba! + ¡Los datos del perfil han sido guardados! + Ninguna acción puede ser tomada + ¡El contenido multimedia ha sido guardado! + ¡Un error ocurrió mientras se traducía! + Las traducciones están desactivadas en la configuración + ¡Borrador guardado! + ¿Estás seguro que esta instancia permite este número de caracteres? Usualmente, este valor es cercano a 500 caracteres. + La visibilidad de los toots ha sido cambiado para la cuenta %1$s + + Número de toots por carga + Siempre + WI-FI + Preguntar + Cargar el contenido multimedia + Cargar las fotos + Mostrar más… + Mostrar menos… + Contenido sensible + Desactivar avatares GIF + Ruta: + Guardar borradores automáticamente + Agregar URL del contenido multimedia en toots + Notificar cuando alguien te siga + Notificar cuando alguien impulse tu estado + Notificar cuando alguien agrega a favoritos tu estado + Notificar cuando alguien te mencione + Notificar cuando termine una encuesta + Notificar nuevas publicaciones + Mostrar diálogo de confirmación antes de impulsar + Mostrar diálogo de confirmación antes de agregar a favoritos + Notificaciones en WI-FI solamente + ¿Notificar? + Notificaciones silenciosas + Tiempo de vista de contenido sensible (segundos, 0 significa terminado) + Tiempo de espera de descripción multimedia (en segundos, 0 para desactivar) + Editar perfil + Compartición personalizada + Tu URL de compartición personalizada… + Bio… + Bloquear cuenta + Guardar cambios + Elegir una imagen de encabezado + Ajustar vista previa de imágenes + Automáticamente divide Toots de más de 500 caracteres en respuestas + ¡Has alcanzado los 160 caracteres permitidos! + ¡Has alcanzado los 30 caracteres permitidos! + Entre + y + Este tiempo debe ser mayor que %1$s + Este espacio de tiempo debe ser menor que %1$s + Iniciar tiempo + Fin + Usar el navegador integrado + Pestañas personalizadas + Permitir Javascripts + Expandir cw automáticamente + Permitir cookies de terceros + Tu llave API, puedes dejar en blanco para Yandex + + Oscuro + Claro + Negro + + Establecer color LED: + + Azul + Cian + Magenta + Verde + Rojo + Amarillo + Blanco + + Seguir + Desbloquear + Silenciar + Dejar de silenciar + Solicitud enviada + Te sigue + Buscar + Primera letra en mayúscula para las respuestas + Redimensionar imágenes + Redimensionar vídeos + + Notificaciones Push + Por favor, confirma notificaciones push que quieres recibir. +Tú puede activar o desactivar esas notificaciones después en configuración (pestaña de notificaciones). + + Borrar caché + Hay %1$s de data en caché.\n\n ¿Quieres eliminarlos? + Mb + ¡Fue borrando el cache! %1$s liberados + + Título + Título… + Descripción + Palabras Claves + Palabras clave… + + Sincronizar + Filtrar + Tus toots + Tus notificaciones + Público + No listados + Privado + Directo + Algunas palabras claves… + Mostrar multimedia + Mostrar fijados + ¡Ningún resultado encontrado! + Respaldar toots de %1$s + %1$s toots nuevos han sido importados + %1$s nuevas notificaciones han sido importadas + + Fechas en orden descendente + Fechas en orden ascendente + + + No + Solamente + Ambos + + No toots fueron encontrados en la base de datos. Por favor, utilice el botón de sincronizar de el menú para recuperarlos. + + Data grabada + Solo la información básica de las cuentas son guardadas en el dispositivo. + Esa data es estrictamente confidencial y solo puede ser usada por la aplicación. + Al borrar la aplicación inmediatamente elimina esa data.\n + ⚠ Usuario y contraseñas nunca son guardados. Estos son solo usados durante la autenticación de seguridad (SSL) con una instancia. + + Permisos: + - ACCESS_NETWORK_STATE: Usado para detectar si el dispositivo esta conectado a una red WI-Fi.\n + - INTERNET: Usado por para una instancia.\n + - ESCRIBIR_ALMACENAMIENTO_EXTERNO: Usado para almacenar contenido multimedia o para mover la aplicación a una tarjeta SD.\n + - LEER_ALMACENAMIENTO_EXTERNO: Usado para agregar contenido multimedia a toots.\n + - INICIO_COMPLETADO: Usado para iniciar servicios de notificación.\n + - WAKE_LOCK: Usado para notificación de servicio. + Permisos API: + - Leer: Leer data.\n + - Escribir: Publicar estados y subir contenido multimedia para estados.\n + - Follow: Seguir, dejar de seguir, bloquear, desbloquear.\n\n + ⚠ Esas acciones son llevadas solo cuando el usuario lo pida + + Rastreadores y librerías + La aplicación no usa herramientas de rastreo (medidores de audiencia, reportadores de error, entre otro) y no contiene ninguna publicidad.\n\n + El uso de librerías es minimizado:\n + -Glide: Para gestionar media\n + -Android-Job: Para gestionar servicios\n +PhotoView Para gestionar imágenes\n + Traducción de los toots + La aplicación ofrece la habilidad de traducir toots usando la localización del dipositivo y el Yandex API\n +Yandex tine su propia política de privacidad la cual puede ser encontrada aquí: +https://yandex.ru/legal/confidential/?lang=en + Gracias a: + Filtrar por expresiones regulares + Buscar + Eliminar + Obtener más toots… + + Listas + ¿Seguro que deseas eliminar esta lista de forma permanente? + No hay ninguna lista aún. Cuando los miembros de esta lista publiquen nuevos estados, apareceran aquí. + Agregar a la lista + Añadir lista + Borrar lista + Editar lista + Nuevo título de lista + ¡La cuenta fue añadida a la lista! + ¡Aún no tienes ninguna lista! + + %1$s se ha movido a %2$s + ¿No funciona la autenticación? + Aquí hay una lista de vistos que pueden ayudar \n\n +-Revisa que no hay ningún error de escritura en la instancia del nombre \n\n +-Revisa que tu instancia no está caída \n\n +-Si tu usas el factor de autenticación de dos pasos (2FA), por favor usa el link al pie (una vez que el nombre de la instancia esta lleno) \n\n +-Puedes usar también este link sin usar el \"Fa \n\n +-Si eso todavía no funciona, por favor notifica el problema en Framagit https://framagit.org/tom79/fedilab/issues + Se ha cargado el archivo. Pulsa aquí para mostrarlo. + Este proceso puede ser bastante largo. Recibirás una notificación cuando esté finalizado. + Todavía en curso, por favor espera… + Exportar estados + Exportar estados de %1$s + %1$s toots de %2$s han sido exportados. + Hubo algún error al exportar los datos de %1$s + ! Algo salió mal al exportar datos! + ! Algo salió mal al importar datos! + + Proxy + ¿Habilitar proxy? + Host + Puerto + Usuario + Contraseña + Agregar datos del toot al compartir + Apoya la aplicación en Liberapay + Hay un error en la expresión regular! + ¡No se encontraron líneas de tiempo en esta instancia! + ¿Borrar esta instancia? + Traducir en + Seguir instancia + ¡Tu ya sigues esta instancia! + ¡La instancia esta siendo seguida! + Asociaciones + Información + Ocultar impulsos de %s + Destacar en el perfil + Mostrar impulsos de %s + No muestre en perfil + La cuenta ha sido promocionada en el perfil + La cuenta ya no aparece en el perfil + ¡Ya se muestran los impulsos! + ¡Los impulsos han sido ocultados! + Mensaje directo + Filtros + No hay filtros para mostrar. Se puede crear uno pulsando en el botón \"+\". + Palabra clave o frase + Linea de tiempo central + Linea de tiempo publica + Notificaciones + Conversaciones + Será igualada independientemente del uso de mayúsculas/minúsculas o de advertencias de contenido de un toot + Soltar en vez de ocultar + Toots filtrados desaparecerán irreversiblemente, aún si el filtro sea removido + Cuando la palabra clave o frase es solamente alfanumérica, solo aplica si coincide con toda la palabra + Palabra completa + Filtrar contenido + Uno o varios contextos donde el filtro debería aplicar + Expirar después de + ¿Eliminar filtro? + Actualizar filtro + Crear filtro + A quiénes seguir + ¡Por el momento no hay cuentas listadas! + Seguir + Seleccionar todo + Deseleccionar todo + ¡%s es seguido! + Creando la lista %s + Añadiendo cuentas a la lista + Las cuentas fueron añadidas a la lista + Añadiendo cuentas a la lista + Todavía no has creado una lista. Pulsa en el botón \"+\" para añadir una nueva. + A quiénes seguir + Trunk API + La(s) cuenta(s) no puede(n) ser seguida(s) + Compilando cuenta remota + Expandir automáticamente contenido multimedia oculto + Nuevo seguidor + Nuevo Impulso + Nuevo Favorito + Nueva Mención + Encuesta Terminada + Nuevo Toot + Respaldo de toots + Nuevas publicaciones + Descargar contenido multimedia + Modificar la notificación de sonido + Seleccionar tono + Activar intervalo de tiempo + Videos de cómo se hace + ¡Obteniendo hilo remoto! + ! No hay dominios bloqueados! + Desbloquear dominio + ¿Estás seguro de que quieres desbloquear a %s? + ¿Está seguro de bloquear %s?\n\nNo verá ningún contenido de ese dominio en ninguna línea de tiempo pública o en sus notificaciones. Tus seguidores de ese dominio serán eliminados. + Dominios bloqueados + Bloquear dominio + El dominio está bloqueado + ¡Este dominio ya no está bloqueado! + Obteniendo estatus remoto + Comentar + Instancia de Peertube + ¡Sé el primero en dejar un comentario en este video con el botón superior derecho! + %s visitas + Duración: %s + Añadir una instancia + ¡Los comentarios no estan habilidatos para este video! + Escoger resolución + Peertube favoritos + ¡El vídeo ha sido añadido a marcadores! + ¡El video ha sido eliminado de tus favoritos! + ¡No hay videos de Peertube en tus favoritos! + Canal + Vídeos + Canales + Usar Emoji One + Información + Mostrar vistas previas de todos los toots + Nuevo diseñador UX/UI + Mostrar previsualización de videos + ¡El identificador de la cuenta ha sido copiado en el portapapeles! + Cambiar idioma + Idioma predeterminado + Truncar toots largos + Truncar los toots sobre \'x\' líneas. Cero significa deshabilitado. + Mostrar más + Mostrar menos + Gestionar etiquetas + ¡Esa etiqueta ya existe! + Se ha guardado la etiqueta. + ¡La etiqueta ha sido cambiada! + ¡Se ha borrado la etiqueta! + Programar impulso + ¡El impulso está programado! + ¡Ningún impulso programado para mostrar! + programar impulso.]]> + Línea de tiempo de arte + Abrir menú + Regresar + Logo de la aplicación + Imagen del perfil + Banner de perfil + Contactar administrador de la instancia + Añadir nueva + Logo de MastoHost + Selección de emoji + Actualizar + Expandir la conversación + Eliminar cuenta + Eliminar el dominio bloqueado + Selector de emoji personalizado + Reproducir video + Nuevo Toot + Imagen de la tarjeta + Ocultar media + Favicon + Medios para agregar una descripción + + Nunca + 30 minutos + 1 hora + 6 horas + 12 horas + 1 día + 1 semana + + En este campo, debe escribir el nombre de host de su instancia. \ Npor ejemplo, si creó su cuenta en https: //mastodon.social \ nSólo escriba mastodon.social (sin https: //) \norte +         Puede comenzar a escribir las primeras letras y se sugerirán los nombres. \ N \ n +         ⚠ El botón Iniciar sesión solo funcionará si el nombre de la instancia es válido y la instancia está activa! + Más información + + Idiomas + Solo media + Mostrar NSFW + Traducciones de Crowdin + Administrador de Crowdin + Traducir la Aplicación + Acerca de Crowdin + Bot + Instancia Pixelfed + Instancia de Mastodon + Cualquiera de estos + Todos estos + Ninguno de estos + Cualquiera de estas palabras (separados por espacios) + Todas estas palabras (separados por espacios) + Agregar algunas palabras para filtrar (separados por espacios) + Cambiar nombre de columna + Instancia Misskey + Ninguna aplicación que soporte este enlace está instalada en su dispositivo. + Suscripciones + Resumen + Tendencias + Añadido recientemente + Local + Subir + Responder + Eliminar un comentario + ¿Estás seguro de eliminar éste comentario? + Video en pantalla completa + Modo para videos + Seleciona un archivo para subir + Mis videos + Título + Licencia + Categoría + Idioma + Este video contiene contenido maduro o explícito + Habilitar comentarios de video + Actualizar vídeo + Descripción + El video se ha actualizado! + ! Carga cancelada! + ¡Se ha subido el video! + Subiendo, espere por favor… + Pulsa aquí para editar los datos del vídeo. + Eliminar vídeo + ¿Está seguro de eliminar este video? + Mostrar vídeos NSFW + ¡No hay videos para ver! + Dejar un comentario + Compartir + Elija un modo de programación + Desde dispositivo + Desde servidor + Toots (Servidor) + Toots (Dispositivo) + Modificar + Mostrar nuevos toots sobre el botón \"Obtener más\" + Líneas de tiempo + Interfaz + Contactos + %1$s comentó tu vídeo %2$s]]> + %1$s está siguiendo tu canal %2$s]]> + %1$s está siguiendo tu cuenta]]> + %1$s ha sido publicado]]> + %1$s ha tenido éxito]]> + %1$s falló]]> + %1$s publicó un nuevo video: %2$s]]> + %1$s esta en lista negra]]> + %1$s ha sido unblacklisted]]> + Exportar datos + Importar Datos + Selecciona el archivo para importar + ! Se produjo un error al seleccionar el archivo de copia de seguridad! + Agrega un comentario público + Enviar comentario + No hay conexión a internet. Su mensaje ha sido almacenado en borradores. + Texto plano + HTML + Markdown + Cerrar de sesión de la cuenta + Todo + Apoya la aplicación + Open Collective, permite a los grupos rápidamente establecer un colectivo, recaudar fondos y administrarlos de forma transparente. + Copiar enlace + Conectar + Normal + Compacta + Consola + Establecer el modo de visualización + Parchar el Proveedor de Seguridad + Actualizar dominios de seguimiento + ¡La base de datos de seguimiento ha sido actualizada! + Llamadas http bloqueadas por la aplicación + Lista de llamadas bloqueadas + Enviar + ¡La base de datos ha sido exportada! + Hashtags destacados + Filtrar línea de tiempo con etiquetas + Sin etiquetas + Ocultar el botón \"eliminar\" en la pestaña de notificación + Adjuntar una imagen al compartir una URL + + Encuesta + Encuestas + Crear una encuesta + Opción 1 + Opción 2 + Opción %d + ¡Necesitas al menos dos opciones para la encuesta! + Hecho + final en %s + Actualizar encuesta + Votar + Una encuesta en la que has votado ha terminado + Una encuesta que usted tooteo ha terminado + Personalizar + Categorías + Intervalo de tiempo + Avanzado + Mostrar insignia \'nuevo\' en los toots no leídos + Peertube + Mover la línea de tiempo + Ocultar la línea de tiempo + Reordenar las líneas de tiempo + Lista eliminada permanentemente + La Instancia seguida ha sido eliminada + Etiqueta anclada eliminada + Deshacer + ¡Necesitas mantener dos pestañas visibles! + Reordenar las líneas de tiempo + ¡Las líneas de tiempo principales sólo pueden ser ocultadas! + BBCode + Marcar siempre los medios como sensibles + Instancia GNU + Estatus en caché + Reenviar etiquetas en respuestas + Pulsación larga para almacenar medios + Desenfocar las imágenes sensibles + Mostrar líneas de tiempo en una lista + Mostrar líneas de tiempo + Marcar las cuentas de bots en los toots + Gestionar etiquetas + Recordar la posición en la línea de tiempo de inicio + Historia + Listas de reproducción + Nombre para mostrar + Usted no tiene ninguna lista de reproducción. Pulsa en el icono \"+\" para agregar una nueva lista de reproducción + ¡Debes proporcionar un nombre a mostrar! + El canal es necesario cuando la lista de reproducción es pública. + Crear una lista de reproducción + No hay nada en esta lista de reproducción todavía. + rehacer + Galería + Emoji + Calcomanía + Borrador + Texto + Filtrar + Pincel + ¿Estás seguro de que deseas salir sin guardar la imagen? + Descartar + Guardando… + ¡Imagen guardada con éxito! + Error al guardar la Imagen + Opacidad + Habilitar el editor de fotos + Añadir un elemento a la encuesta + Eliminar último elemento de la encuesta + Silenciar conversación + Desactivar el silencio de la conversación + ¡La conversación ya no está silenciada! + La conversación está silenciada + Abrir las características de la Aplicación + Silenciador temporizado + Mencionar la cuenta + Actualizar el caché + Mencionar el estatus + Noticias + General + Regional + Arte + Periodismo + Activismo + Juegos + Tecnología + Contenido para adultos + Peludo + Comida + Logotipo de la instancia + ¡Algo salió mal al comprobar las instancias disponibles! + Unirse a Mastodon + Elija una instancia escogiendo una categoría, y luego pulsa en un botón de verificación. + Elegir una instancia pulsando en un botón de selección. + %1$s usuarios + Confirmar la contraseña + Estoy de acuerdo con %1$s y %2$s + reglas del servidor + Términos del servicio + Registrarse + Esta instancia funciona por invitación. Tu cuenta necesita ser aprovada manualmente por un administrador antes de poderse utilizar. + ¡Por favor, rellena todos los campos! + ¡Las contraseñas no coinciden! + ¡El correo electrónico no parece ser válido! + Su nombre de usuario será único en %1$s + Se le enviará un correo de confirmación + Utilice al menos 8 caracteres + La contraseña debe tener al menos 8 caracteres + Los nombres de usuario solo pueden contener letras, números y guiones bajos + ¡Cuenta creada! + ¡Su cuenta ha sido creada!\n\n + Piense en validar su correo electrónico dentro de las próximas 48 horas.\n\n + ahora puedes conectar tu cuenta escribiendo %1$s en el primer campo y pulsando en Conectar.\n\n + Importante: Si tu instancia requiere validación, usted recibirá un correo electrónico una vez que haya sido validada! + + ¿Guardar el mensaje en borradores? + Administración + Reportes + ¡No hay reportes que mostrar! + Reconectar la cuenta + La aplicación no ha podido acceder a las funciones de administrador. Puede que necesite volver a conectar la cuenta para tener el alcance correcto. + No resuelto + Remoto + Activo + Pendiente + Deshabilitado + Silenciado + Suspendido + Permisos + Estatus del correo electrónico + Estado de inicio de sesión + Se unió en + IP más reciente + Advertir + Desactivar + Silenciar + Notificar al usuario por correo electrónico + Advertencia personalizada + Usuario + Moderador + Administrador + Confirmado + No confirmado + Estados reportados + Cuenta + Deshacer silencio + Deshacer deshabilitar + Suspender + Deshacer suspender + ¡La cuenta está silenciada! + ¡La cuenta ya no está silenciada! + ¡La cuenta está suspendida! + ¡La cuenta ya no está suspendida! + ¡La cuenta está deshabilitada! + ¡La cuenta ya no está deshabilitada! + ¡La cuenta ha sido advertida! + Mostrar el menú de administración + Mostrar la función de administrador en estados + Permitir + ¡La cuenta está aprobada! + ¡La cuenta ha sido rechazada! + Asignar a mí + Desasignar + Marcar como resuelto + Marcar como no resuelto + ¡Contenido vacío! + Mostrar botón de características de Fedilab + La aplicación necesita acceder a la grabación de audio + Mensaje de voz + Activar respuesta rápida + ¡La cuenta a la que estás respondiendo podría no ver tu mensaje! + Si está desactivado, la aplicación siempre cargará los últimos estados + Si está desactivado, el contenido multimedia sensible será ocultado con un botón + Almacenar contenido multimedia a tamaño completo con una pulsación larga en la previsualización + Agregar un botón de elipse en la parte superior derecha para listar todas las etiquetas/instancias/listas + Durante el intervalo de tiempo, la aplicación enviará notificaciones. Puede revertir (es decir: silenciar) este intervalo de tiempo con el botón derecho. + Mostrar un botón de Fedilab debajo de la imagen de perfil. Es un acceso directo para acceder a las características de la aplicación. + Permite responder directamente en las líneas de tiempo debajo de los estados + Las vistas previas no se recortarán en las líneas de tiempo + Permite reproducir videos incrustados directamente en las líneas de tiempo + Permite invertir la forma de leer los estados que se muestran al pulsar el botón de obtener más + Esta opción da soporte a las suites recientes de cifrado. Es útil para dispositivos Android antiguos o si no se puede conectar a su instancia. + Exclusivamente para videos de Peertube. Cambia este modo si no puedes reproducirlos. + Estas etiquetas permitirán filtrar los estados de los perfiles. Tendrá que usar el menú contextual para verlos. + Insertar automáticamente un salto de línea después de la mención para poner en mayúscula la primera letra + Permitir a los creadores de contenido compartir los estados en sus fuentes RSS + Redactar + Tiempo máximo de reintentos al subir contenido multimedia + Crear una nueva Carpeta aquí + Introduzca el nombre de la carpeta + Por favor, introduzca un nombre de carpeta válido + Esta carpeta ya existe.\n Por favor, proporcione otro nombre para la carpeta + Seleccionar + Directorio por defecto + Carpeta + Crear carpeta + ¿Mostrar mensaje de felicitación tras completar una acción (impulsar, favorito, etc.)? + ¡Se han exportado las instancias silenciadas! + Añadir una instancia + Exportar instancias + Importar instancias + Informes de fallos + Habilitar informes de fallos + Si está habilitado, un informe de fallos se creará localmente y entonces podrás compartirlo. + Fedilab se ha detenido :( + Me puedes enviar por correo electrónico el informe de fallas. Ayudará a solucionarlo :)\n\n puedes agregar contenido adicional. ¡Gracias! + Usar WYSIWYG (lo que ves es lo que obtienes) + Cuando esté habilitado, podrás formatear tu texto fácilmente con herramientas. + Estadísticas + Total de estados + Número de impulsos + Número de favoritos + Número de menciones + Número de seguidos + Número de encuestas + Número de respuestas + Número de estados + Estados + Visibilidad + Número con multimedia + Número con multimedia sensible + Número con CW + Fecha del primer estado + Fecha del último estado + Fecha de la primera notificación + Fecha de la última notificación + Frecuencia + %s estados por día + %s notificaciones por día + Intervalo de fechas + Grupos + ¡No hay grupos! + Deshabilitar emojis animados personalizados + Gráficas + Mostrar gráficas + La aplicación recopila sus datos locales, por favor espere... + Respaldo + Estatus del auto respaldo + Esta opción es por cuenta. Se lanzará un servicio que almacenará automáticamente sus estados de forma local en la base de datos. Esto permite obtener estadísticas y gráficos + Copia de seguridad automática de notificaciones + Esta opción es por cuenta. Se pondrá en marcha un servicio que almacenará automáticamente las notificaciones de forma local en la base de datos. Esto permite obtener estadísticas y gráficos + Reportar cuenta + Enviar invitación + ¡Tu instancia no permite registrar una nueva cuenta! + + %d voto + %d voto + + + %d votante + %d votantes + + + Selección única + Opciones múltiples + + + 5 minutos + 30 minutos + 1 hora + 6 horas + 1 día + 3 días + 7 días + + + WebView + Flujo directo + + Para unirte a mi instancia \"%1$s\", puedes descargar Fedilab:\n\nF-Droid: %2$s\nGoogle: %3$s\n\nLuego abre el enlace siguiente con Fedilab y crea tu cuenta :)\n\n%4$s + + ¡Tu encuesta no puede tener opciones duplicadas! + Para todas las cuentas + Caché de la base de datos + Limpia tu caché de la línea de tiempo de inicio + Limpia tus estados en caché + Eliminar tus marcadores + Archivos en caché + Total de notificaciones + Ocultar elementos del menú + Fedilab está ejecutando notificaciones en vivo + Para %1$s cuentas con %2$s eventos + Notificaciones en vivo para %1$s + Las notificaciones en vivo sólo estarán desactivadas para esta cuenta. + Borrar caché al salir + El caché (archivos multimedia, mensajes en caché, datos del navegador integrado) se borrará automáticamente al salir de la aplicación. + ¿Desea dejar de seguir esta cuenta? + Mostrar diálogo de confirmación antes de dejar de seguir + Reemplazar Youtube con Invidio.us + Invidious es una interfaz alternativa a YouTube + Introduce tu host personalizado o deja en blanco para usar invidio.us + Reemplazar Twitter con Nitter + Nitter es una alternativa de código abierto a la interfaz de Twitter enfocada en la privacidad. + Introduce tu host personalizado o deja en blanco para usar nitter.net + Reemplazar Instagram con Bibliogram + Bibliogram es una interfaz alternativa de Instagram de código abierto centrada en la privacidad. + Introduzca su servidor personalizado o déjelo en blanco para usar bibliogram.art + Reemplazar Reddit con Libreddit + Libreddit es una interfaz alternativa de código abierto de Reddit enfocada en la privacidad. + Introduce tu servidor personalizado o déjalo en blanco para usar libredd.it + Reemplazar enlaces a Medium + Reemplazar los enlaces a medium.com con un front-end alternativo de código abierto centrado en la privacidad. + Por defecto: scribe.rip + Reemplazar enlaces a Wikipedia + Reemplace enlaces a Wikipedia con un front-end alternativo de código abierto centrado en la privacidad. + Por defecto: wikiless.org + Ocultar la barra de notificaciones de Fedilab + Para ocultar el resto de la notificación en la barra de estado, presione en el botón con el ícono de un ojo y luego desactivar \"Mostrar en la barra de estado\" + Usa un sistema de notificaciones push para obtener notificaciones en tiempo real. + Sin notificaciones en vivo + Notificaciones en vivo + Se buscarán notificaciones cada 15 minutos. + Añadir notas + Notas de la cuenta + Permitir la compresión de imágenes de gran tamaño a imágenes más pequeñas con poca o ninguna pérdida de calidad. + Permite comprimir videos manteniendo su calidad. + La aplicación está comprimiendo el archivo multimedia, puede tardar un rato… + Cambio de icono de la aplicación + Pulsa para cambiar el icono de la aplicación + Publicar + Visibilidad de la publicación + Pulsa aquí para añadir fotos + Formatos aceptados: jpeg, png, gif \n\nTamaño máximo de archivo: 15 MB \n\nLos álbumes pueden contener hasta 4 fotos o videos + Subir contenido multimedia + Añadir una descripción opcional + La aplicación recibió un mensaje de error muy largo de la API %1$s + Vista previa de mensaje + Añadir menciones en cada mensaje + Obteniendo conversación + Ordenar por + Título del vídeo + Unirse a Peertube + Tengo al menos 16 años y estoy de acuerdo con los %1$s de esta instancia + Enlaces + Cambiar el color de los enlaces (Url, menciones, etiquetas, etc.) en los mensajes + Cabecera de impulsos + Cambiar el color del nombre mostrado en la parte superior de los mensajes + Cambiar el color del nombre de usuario en la parte superior de los mensajes + Cambiar el color de la cabecera de los impulsos + Publicaciones + Color de fondo de las publicaciones en las líneas de tiempo + Restablecer colores + Pulsa aquí para restablecer todos tus colores personalizados + Restablecer + Iconos + Color de los iconos inferiores en las líneas de tiempo + Fijar esta etiqueta + Logo de la instancia + Editar perfil + Realizar una acción + Traducción + Vista previa de imagen + Color del texto + Cambiar el color del texto en las publicaciones + Aplicar cambios + Necesitas reiniciar la aplicación para aplicar los cambios + Reiniciar + Usar un tema personalizado + Permiten reemplazar los colores del tema seleccionado anteriormente + Personalización + Almacenar previamente + El tema ha sido exportado + El tema se ha exportado correctamente en CSV + Aplicar el color principal a la barra de estado + Color de la barra de estado + Restaurar un tema predeterminado + Importar un tema + Pulsa aquí para importar un tema exportado previamente + Exportar el tema + Pulsa aquí para exportar el tema actual + Se produjo un error al seleccionar el archivo de tema + Selector de Temas + Selecciona un tema pre-instalado + Temas + Aplicar el color principal a la barra de navegación + Color de la barra de navegación + El color de fondo del contenido de la aplicación. + Color de fondo + Se aplica a ciertas partes de la interfaz. + Color de acento + Mostrado más frecuentemente por tu aplicación. + Color principal + Exportar marcadores a la instancia + Importar marcadores desde la instancia + Número de usuarios + Número de estados + Número de instancias + Bloqueado + Finaliza en %s + Novedades en %s + Puedes seguir mi cuenta para mantenerte al día con las actualizaciones + Esta instancia no está disponible en https://instances.social + Mostrar enlace completo + Compartir enlace + La URL se ha copiado al portapapeles + Abrir con otra aplicación + Comprobar redirección + El redireccionamiento de la URL no funciona + %1$s \n\nredirige a\n\n %2$s + Cambiar el agente de usuario + Establece un agente de usuario personalizado o deja en blanco + Permite personalizar el agente de usuario que se utiliza para las llamadas a la api o con el navegador integrado. + Eliminar parámetros UTM + La aplicación eliminará automáticamente los parámetros UTM de las URLs antes de visitar un enlace. + Tendencias + Tendencia ahora + %d personas hablando + Cuentas de Twitter (vía Nitter) + Nombres de usuario de Twitter separados por un espacio + Pruebas de identidad + Identidad verificada + Verificado por %1$s (%2$s) + Eliminar la notificación + Mostrar más opciones + Es una historia de Pixelfed + Sube un archivo multimedia, se añadirá automáticamente a tu historia de Pixelfed. + ¡Archivos multimedia añadidos con éxito a tu historia! + Acción desactivada + Dejar de seguir + Algo salió mal, por favor compruebe su directorio de descargas en la configuración. + Anuncios + ¡No hay anuncios! + Agregar una reacción + Usa tu navegador favorito dentro de la aplicación. Desmarca esta función para abrir enlaces externamente. + Caché de vídeo en MB, cero significa que no hay caché. + Marcas de agua + Añadir automáticamente una marca de agua en la parte inferior de las imágenes. El texto se puede personalizar para cada cuenta. + ¡No se encontraron distribuidores! + Necesitas un distribuidor para recibir notificaciones push.\nEncontrarás más detalles en %1$s.\n\nTambién puedes desactivar las notificaciones push en la configuración para ignorar ese mensaje. + Selecciona un distribuidor + diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml new file mode 100644 index 000000000..9a7c0a7c1 --- /dev/null +++ b/app/src/main/res/values-eu/strings.xml @@ -0,0 +1,1141 @@ + + + Ireki menua + Itxi menua + Honi buruz + Instantziari buruz + Pribatutasuna + Cachea + Amaitu saioa + Hasi saioa + + Itxi + Bai + Ez + Utzi + Deskargatu + Deskargatu %1$s + Multimedia gordeta + Fitxategia: %1$s + Pasahitza + E-maila + Kontuak + Toot-ak + Etiketak + Gorde + Berrezarri + Emaitzarik ez! + Instantzia + Instantzia: mastodon.social + Orain %1$s kontuarekin dabil + Gehitu kontu bat + Toot-aren edukia arbelera kopiatu da + Toot-aren URL-a arbelera kopiatu da + Aldatu + Hautatu irudi bat… + Garbitu + Kamera + Ezabatu guztiak + Itzuli toot hau. + Programatu + Testuaren eta ikonoen tamainak + Aldatu testuaren tamaina: + Aldatu ikonoaren tamaina: + Hurrengoa + Aurrekoa + Honekin ireki + Onartu + Multimedia + Partekatu honekin + Fedilab bidez partekatua + Erantzunak + Erabiltzaile-izena + Zirriborroak + Gogokoak + Jarraitzaile berriak + Aipamenak + Bultzadak + Erakutsi bultzadak + Erakutsi erantzunak + Ireki nabigatzailean + Itzuli + Itxaron segundo batzuk ekintza hau abiatu aurretik. + + Hasiera + Tokiko denbora-lerroa + Federatutako denbora-lerroa + Aukerak + Gogokoak + Komunikazioa + Mutututako erabiltzaileak + Blokeatutako erabiltzaileak + Jakinarazpenak + Jarraitzeko eskaerak + Ezarpenak + Ezabatu kontu bat + Ezabatu %1$s kontua aplikaziotik? + Bidali e-mail bat + Egin klik bide-izenean berau aldatzeko + Huts egin du! + Programatutako toot-ak + Beheko informazioak ez du erabiltzailearen profila bere osotasunean adierazten. + Txertatu emojia + Aplikazioak ez ditu emoji pertsonalizatuak jaso oraingoz. + Push notifications + Ziur saioa amaitu nahi duzula? + Ziur @%1$s@%2$s saioa amaitu nahi duzula? + + Ez dago toot-ik bistaratzeko + Ez dago istoriorik bistaratzeko + Istorioak + %1$s(e)k bultzatua + Gehitu toot hau zure gogokoetara? + Kendu toot hau zure gogokoetatik? + Bultzada eman toot honi? + Bultzada kendu toot honi? + Finkatu toot hau? + Desfinkatu toot hau? + Mututu + Blokeatu + Salatu + Kendu + Kopiatu + Partekatu + Aipatu + Denbora baterako mutututa + Ezabatu eta berridatzi + + Mututu kontu hau? + Blokeatu kontu hau? + Salatu toot hau? + Blokeatu domeinu hau? + Desmututu kontu hau? + Desblokeatu kontu hau? + + + Jakinarazi + Isilik + + + Kendu toot hau? + Ezabatu eta berridatzi toot hau? + + Gogokoak + Gehitu gogokoetara + Kendu gogokoa + Ez dago gogokorik bistaratzeko + Mezua gogokoetara gehitu da! + Mezua gogokoetatik kendu da! + + %d s + %d m + %d h + %d e + + Segundo %d + %d segundo + + + Minutu %d + %d minutu + + + Ordu %d + %d ordu + + + Egun %d + %d egun + + + Abisua + Zer duzu buruan? + TOOT! + QUEET! + CW + Idatzi toot bat + Erantzun toot bati + Idatzi queet bat + Erantzun queet bat + Hautatu multimedia bat + Errorea gertatu da multimedia aukeratzean! + Ezabatu multimedia hau? + Zure toot-a hutsik dago! + Toot-aren ikusgaitasuna + Toot-en lehenetsitako ikusgaitasuna: + Toot-a bidali da! + Toot honi erantzuten ari zara: + Eduki hunkigarria? + + Argitaratu denbora-lerro publikoetan + Ez argitaratu denbora-lerro publikoetan + Bidali jarraitzaileei besterik ez + Bidali aipatutako erabiltzaileei besterik ez + + Zirriborrorik ez! + Hautatu toot bat + Hautatu kontu bat + Hautatu kontu batzuk + Kendu zirriborroa? + Sakatu botoia jatorrizko toot-a erakusteko + Deskribatu ikusmen arazoak dituztenentzat + + Ez dago deskripziorik eskuragarri! + + Bertsioa: %1$s + Garatzailea: + Lizentzia: + GNU GPL V3 + Iturburu kodea: + Toot-en itzulpena: + Bilatu instantziak: + Ikonoaren diseinatzailea: + + Elkarrizketa + + Bistaratzeko konturik ez + Jarraipen eskaririk ez + Toot-ak \n %1$s + Jarraitzen \n %1$s + Jarraitzaileak \n %1$s + Finkatua \n %d + Baimendu + Ukatu + + Ez dago programatutako toot-ik bistaratzeko! + Idatzi toot bat eta hautatu Programatu goiko menuan. + Ezabatu programatutako toot-a? + Multimedia: %d + Toot-a programatu da! + Programatutako data orain baino beranduago izan behar du! + Bateria aurreztea gaituta dago! Agian ez dabil behar bezala. + + Mututzeko denbora minutu bat baino gehiago izan behar da. + %1$s mututu da %2$s arte.\n Kontu hau desmututu dezakezu bere profiletik. + %1$s mututu da %2$s arte.\n Egin klik hemen kontu hau desmututu nahi baduzu. + + Jakinarazpenik ez bistaratzeko + erabiltzaileak aipatu zaitu + wrote a new message + erabiltzaileak zure mezuari bultzada eman dio + erabiltzaileak zure mezua gogoko du + zu jarraitzen hasi da + zu jarraitzea eskatu du + + eta beste jakinarazpen bat + eta beste %d jakinarazpen + + + gogoko %d + %d gogoko + + Ezabatu jakinarazpen bat? + Ezabatu jakinarazpen guztiak? + Jakinarazpena ezabatu da! + Jakinarazpen guztiak ezabatu dira! + + Jarraitzen + Jarraitzaileak + Finkatuta + + Ezin izan da bezero id-a eskuratu! + Ezin izan da instantziaren domeinura konektatu! + Ez dago Internet konexiorik! + Kontua blokeatua izan da! + Kontua ez dago jada blokeatuta! + Kontua mututua izan da! + Kontua ez dago jada mututua! + Kontua jarraitzen hasi zara! + Ez duzu jada kontu hau jarraitzen! + Toot-ari bultzada eman zaio! + Toot-ari bultzada kendu zaio! + Toot-a zure gogokoetara gehitu da! + Toot-a zure gogokoetatik kendu da! + Toot-a salatu da! + Toot-a ezabatu da! + Toot-a finkatu da! + Toot-a desfinkatu da! + Errore bat gertatu da! + Errore bat gertatu da! Instantziak ez du baimen koderik itzuli! + Instantziaren domeinua ez dirudi baliozkoa! + Errore bat gertatu da kontuen artean aldatzean! + Errore bat gertatu da bilatu bitartean! + Profileko datuak gorde dira! + Ezin da ekintzarik egin + Multimedia gorde da! + Errore bat gertatu da itzuli bitartean! + Itzulpenak ezarpenetan desgaituta daude + Zirriborroa gordeta! + Ziur instantzia honek hainbeste karaktere onartzen dituela? Orokorrean, balio hau 500 inguru izaten da. + %1$s kontuaren toot-en ikusgaitasuna aldatu da + + Toot kopurua kargako + Beti + WIFI + Galdetu + Kargatu multimedia + Kargatu irudiak + Erakutsi gehiago… + Erakutsi gutxiago… + Eduki hunkigarria + Desgaitu GIF abatarrak + Bidea: + Gorde zirriborroak automatikoki + Gehitu multimediaren URLa toot-etan + Jakinarazi norbaitek jarraitzen dizunean + Jakinarazi norbaitek zure mezuari bultzada ematean + Jakinarazi norbaitek zure mezua gogoko duenean + Jakinarazi norbaitek aipatzen zaituenean + Jakinarazi inkesta bat amaitu denean + Notify for new posts + Baieztatu bultzada eman aurretik + Baieztatu gogokoetara gehitu aurretik + Jakinarazi WIFI bidez besterik ez + Jakinarazi? + Jakinarazpen isilak + NSFW ikusteko denbora-muga (segundoak, 0 itzalita) + Multimediaren deskripzioaren denbora-muga (segundoak, 0 desgaituta esan nahi du) + Editatu profila + Partekatze pertsonalizatua + Zure partekatze pertsonalizaturako URL-a… + Bio… + Blokeatu kontua + Gorde aldaketak + Hautatu goibururako irudi bat + Doitu irudien aurrebistak + Zatitu automatikoki 500 karakteretik gorako toot-ak erantzunetan + Baimendutako 160 karaktereetara heldu zara! + Baimendutako 30 karaktereetara heldu zara! + Nondik + nora + Denbora %1$s baino gehiago izan behar da + Denbora %1$s baino gutxiago izan behar da + Hasiera ordua + Amaiera ordua + Erabili aplikazio barneko nabigatzailea + Fitxa pertsonalak + Gaitu JavaScript + Hedatu automatikoki CW + Baimendu hirugarrengoen cookie-ak + Zure API gakoa, Yandex erabiltzeko hutsik laga dezakezu + + Iluna + Argia + Beltza + + Ezarri LEDaren kolorea: + + Urdina + Ziana + Magenta + Berdea + Gorria + Horia + Zuria + + Jarraitu + Desblokeatu + Mututu + Desmututu + Eskaera bidalita + Zu jarraitzen + Bilatu + Lehen letra maiuskulaz erantzunetan + Aldatu irudien tamaina + Aldatu tamaina bideoei + + Push jakinarazpenak + Berretsi jaso nahi dituzun push jakinarazpenak. + Jakinarazpen hauek gero ezarpenetan aktibatu edo desaktibatu ditzakezu (Jakinarazpenak fitxa). + + + Garbitu cache-a + %1$s datu daude cachean.\n\nEzabatu nahi dituzu? + Mb + Cache-a garbitu da! %1$s askatu dira + + Izenburua + Izenburua… + Deskripzioa + Hitz gakoak + Hitz gakoak… + + Sinkronizatu + Iragazi + Zure toot-ak + Zure jakinarazpenak + Publikoa + Zerrendatu gabea + Pribatua + Zuzena + Hitz gako batzuk… + Erakutsi multimedia + Erakutsi finkatuta + Ez da emaitzarik aurkitu! + Egin %1$s kontuko toot-en babes-kopia + %1$s toot inportatu dira + %1$s jakinarazpen berri inportatu dira + + Data behera + Data gora + + + Ez + Bakarrik + Biak + + Ez da toot-ik aurkitu datu-basean. Erabili menuko sinkronizatu botoia eskuratzeko. + + Grabatutako datuak + Kontuen oinarrizko informazioa besterik ez da gordetzen gailuetan + Datu hauek guztiz konfidentzialak dira eta bakarrik aplikazioak erabili ditzake. + Aplikazioa ezabatzean datu hauek ere ezabatzen dira.\n + ⚠ Saioa eta pasahitzak ez dira inoiz gordetzen. Instantzia batekin autentifikazio segurua (SSL) ezartzean besterik ez dira erabiltzen. + + Baimenak: + - ACCESS_NETWORK_STATE: Gailua WIFI sare batera konektatuta dagoen egiaztatzeko erabiltzen da.\n + - INTERNET: Instantzia bati itaunketak egiteko erabiltzen da.\n + - WRITE_EXTERNAL_STORAGE: Multimedia gordetzeko edo aplikazioa SD txartelera eramateko erabiltzen da.\n + - READ_EXTERNAL_STORAGE: Toot-etara multimedia gehitzeko erabilytzen da.\n + - BOOT_COMPLETED: Jakinarazpen zerbitzua abiatzeko erabiltzen da.\n + - WAKE_LOCK: Jakinarazpen zerbitzuan zehar erabiltzen da. + + APIaren baimenak: + - Irakurri: Irakurri datuak.\n + - Idatzi: Bidali mezuak eta igo mezuetarako multimedia.\n + - Jarraitu: Jarraitu, jarraitzeari utzi, blokeatu, desblokeatu.\n\n + ⚠ Ekintza hauek erabiltzaileak eskatuta besterik ez dira burutzen. + + Jarraipena eta liburutegiak + Aplikazioak ez du jarraipen tresnarik erabiltzen (audientziaren neurria, erroreen jakinarazpena, eta abar.) eta ez du publizitaterik.\n\n + Liburutegien erabilera minimizatu da: \n + - Glide: Multimedia kudeatzeko\n + - Android-Job: Zerbitzuak kudeatzeko\n + - PhotoView: Irudiak kudeatzeko\n + + Toot-en itzulpena + Aplikazioak ttot-ak itzultzeko aukera ematen du gailuaren locale eta Yandex API-a erabiliz.\n + Yandex-ek bere pribatutasun politika du eta hemen aurkitu daiteke: https://yandex.ru/legal/confidential/?lang=en + + Eskerrik asko: + + Iragazi adierazpen erregularrak erabiliz + Bilatu + Ezabatu + Jaso toot gehiago… + + Zerrendak + Ziur betiko ezabatu nahi duzula zerrenda hau? + Ez dago ezer zerrenda honetan. Zerrenda honetako kideek mezu berriak bidaltzean, hemen agertuko dira. + Gehitu zerrendara + Gehitu zerrenda + Ezabatu zerrenda + Editatu zerrenda + Zerrenda berriaren izena + Kontua zerrendara gehitu da! + Ez duzu zerrendarik oraindik! + + %1$s %2$s(e)ra mugitu da + Autentifikazioa ez dabil? + Hona lagundu dezaketen egiaztaketa batzuk:\n\n + - Egiaztatu ez dagoela akatsik instantziaren izenean\n\n + - Egiaztatu instantzia ez dagoela erorita\n\n + - Bi faktoreetako autentifikazioa erabiltzen baduzu (2FA), erabili beheko esteka (behin instantziaren izena bete eta gero)\n\n + - Esteka hori erabili dezakezu 2FA erabiltzen ez baduzu ere\n\n + - Oraindik ez badabil, ireki arazo bat Framagit kontu honetan https://framagit.org/tom79/fedilab/issues + + Multimedia kargatu da. Egin klik hemen berau bistaratzeko. + Ekintza honek denbora behar dezake. Bukatzean jakinaraziko zaizu. + Oraindik abian, itxaron mesedez… + Esportatu mezuak + Esportatu %1$s(e)ren mezuak + %1$s toot esportatu dira %2$stik. + Zerbait ez da behar bezala joan %1$s(e)ko datuak esportatzean + Zerbait ez da behar bezala joan datuak esportatzean! + Zerbait ez da behar bezala joan datuak inportatzean! + + Proxya + Gaitu proxya? + Ostalaria + Ataka + Saioa + Pasahitza + Gehitu toot-aren xehetasunak partekatzean + Babestu aplikazioa Liberapay bidez + Errore bat dago adierazpen erregularrean! + Ez da denbora-lerrorik aurkitu instantzia honetan! + Ezabatu instantzia hau? + Itzuli + Jarraitu instantzia + Dagoeneko jarraitzen duzu instantzia hau! + Instantzia jarraituta! + Lankidetzak + Informazioa + Ezkutatu %s(r)en bultzadak + Nabarmendu profilean + Erakutzi %s(r)en bultzadak + Ez nabarmendu profilean + Kontu hau orain profilean nabarmentzen da + Kontu hau ez da jada profilean nabarmentzen + Bultzadak orain erakusten dira! + Bultzadak orain ezkutatzen dira! + Mezu zuzena + Iragazkiak + Ez dago iragazkirik bistaratzeko. Bat sortu dezakezu \"+\" botoia sakatuz. + Hitz gakoa edo esaldia + Hasierako denbora-lerroa + Denbora-lerro publikoak + Jakinarazpenak + Elkarrizketak + Bat egingo du testua eta toot-aren eduki abisua maiuskulak zein minuskulak erabiltzen baditu + Bota ezkutatu ordez + Iragazitako toot-ak behin betiko desagertuko dira, gero iragazkia kentzen bada ere + Hitz gakoa edo esaldia soilik alfanumerikoa bada, bakarrik hitz osoarekin bat datorrenean aplikatuko da + Hitz osoa + Iragazkien testuinguruak + Iragazkia aplikatzeko testuinguru bat edo gehiago + Iraungitze data + Ezabatu iragazkia? + Eguneratu iragazkia + Sortu iragazkia + Nor jarraitu + Ez dago konturik zerrendan oraindik! + Jarraitu + Hautatu denak + Deshautatu denak + %s jarraituta! + %s zerrenda sortzen + Kontuak zerrendara gehitzen + Kontuak zerrendara gehitu dira + Kontuak zerrendara gehitzen + Ez duzu zerrendarik gehitu oraindik. Sakatu \"+\" botoia berri bat gehitzeko. + Nor jarraitu + Trunk APIa + Ezin da/dira kontua/k jarraitu + Urruneko kontua eskuratzen + Hedatu automatikoki ezkutatutako multimedia + Jarraipen berria + Bultzada berria + Gogoko berria + Aipamen berria + Inkesta amaituta + Toot berria + Toot-en babes-kopia + New posts + Multimediaren deskarga + Aldatu jakinarazpen-soinua + Hautatu doinua + Gaitu denbora-tartea + Laguntza bideoak + Urruneko haria eskuratzen! + Ez dago blokeatutako domeinurik! + Desblokeatu domeinua + Ziur al zaude %s desblokeatu nahi duzula? + Ziur al zaude %s blokeatu nahi duzula? + Blokeatutako domeinuak + Blokeatu domeinua + Domeinu hau blokeatuta dago + Domeinua ez dago jada blokeatuta! + Urruneko mezua jasotzen + Iruzkina + Peertube instantzia + Izan bideo honetan iruzkin bat uzten lehena goian eskuinean dagoen botoiarekin! + %s ikustaldi + Iraupena: %s + Gehitu instantzia bat + Iruzkinak ez daude aktibatuta bideo honetan! + Hautatu bereizmen bat + Peertubeko gogokoak + Bideoa gogokoetara gehitu da! + Bideoa gogokoetatik kendu da! + Ez dago Peertubeko bideorik gogokoetan! + Kanala + Bideoak + Kanalak + Erabili Emoji One + Informazioa + Erakutsi aurrebistak toot guztietan + Interfazearen diseinatzaile berria + Erakutsi bideoen aurrebista + Kontuaren id-a arbelera kopiatu da! + Aldatu hizkuntza + Lehenetsitako hizkuntza + Moztu toot luzeak + Moztu \'x\' lerro baino gehiago dituzten tootak. Zerok desaktibatuta dagoela esan nahi du. + Erakutsi gehiago + Erakutsi gutxiago + Kudeatu etiketak + Etiketa badago aurretik! + Etiketa gorde da! + Etiketa aldatu da! + Etiketa ezabatu da! + Programatu bultzada + Bultzada programatu da! + Ez dago programatutako bultzadarik bistaratzeko! + programatu bultzada.]]> + Arte denbora-lerroa + Ireki menua + Joan atzera + Aplikazioaren logoa + Profilaren Irudia + Profileko banda + Kontaktatu instantziaren administratzailearekin + Gehitu berria + MastoHost logoa + Emoji hautatzailea + Berritu + Hedatu elkarrizketa + Kendu kontu bat + Ezabat blokeatutako domeinua + Emoji pertsonalizatuen hautatzailea + Erreproduzitu bideoa + Toot berria + Txartelaren irudia + Ezkutatu multimedia + Gune-ikurra + Deskripzioa gehitzeko multimedia + + Inoiz ez + 30 minutu + ordu 1 + 6 ordu + 12 ordu + egun 1 + aste 1 + + Eremu honetan, zure instantziaren hostalari izena idatzi behar duzu.\nAdibidez, zure kontua https://mastodon.eus helbidean sortu baduzu\nIdatzi mastodon.eus (https:// gabe)\n + Idazten hasi zaitezke eta izenak proposatuko zaizkizu.\n\n + ⚠ Konektatu botoiak instantzia baliogarria bada eta martxan badago funtzionatuko du, bestela ez! + + Informazio gehiago + + Hizkuntzak + Multimedia besterik ez + Erakutsi NSFW + Crowdin itzulpenak + Crowdin kudeatzailea + Aplikazioaren itzulpena + Crowdin-i buruz + Bota + Pixelfed instantzia + Mastodon instantzia + Hauetako edozein + Hauek guztiak + Hauetako bat ere ez + Hitz hauetako edozein (zuriunez banatuta) + Hitz hauek guztiak (zuriunez banatuta) + Gehitu iragazi beharreko hitzak (zuriuneekin bereizita) + Aldatu zutabearen izena + Misskey instantzia + Ez dago esteka hau onartzen duen aplikaziorik zure gailuan instalatuta. + Harpidetzak + Bista orokorra + Joerak + Berriki gehituta + Tokikoa + Igo + Erantzun + Ezabatu iruzkin bat + Ziur iruzkin hau ezabatu nahi duzula? + Pantaila osoko bideoa + Bideorako modua + Hautatu igo nahi duzun fitxategia + Nire bideoak + Izenburua + Lizentzia + Kategoria + Hizkuntza + Bideo honek helduei zuzendutako edukia du + Gaitu bideoen iruzkinak + Eguneratu bideoa + Deskribapena + Bideoa eguneratua izan da! + Igoera ezeztatuta! + Bideoa igo da! + Igotzen, itxaron mesedez… + Sakatu hemen bideoaren datuak editatzeko. + Ezabatu bideoa + Bideo hau ezabatu nahi duzula ziur al zaude? + Erakutsi bideo hunkigarriak + Ez dago bideorik bistaratzeko! + Egin iruzkin bat + Partekatu + Hautatu programazio modua + Gailutik + Zerbitzaritik + Tootak (zerbitzaria) + Tootak (gailua) + Aldatu + Erakutsi toot berriak \"jaso gehiago\" botoiaren gainetik + Denbora-lerroak + Interfazea + Kontaktuak + %1$s erabiltzaileak zure %2$s bideoan iruzkina egin du]]> + %1$s erabiltzaileak zure %2$s kanala jarraitzen du]]> + %1$s erabiltzaileak zure kontua jarraitzen du]]> + %1$s bideoa argitaratu da]]> + %1$s bideoa ongi inportatu da]]> + %1$s bideoa inportatzean huts egin du]]> + %1$s erabiltzaileak %2$s bideo berria argitaratu du]]> + %1$s bideoa zerrenda beltzean sartu da]]> + %1$s bideoa zerrenda beltzetik kendu da]]> + Esportatu datuak + Inportatu datuak + Hautatu inportatu beharreko fitxategia + Errore bat gertatu da babes-kopia fitxategi bat hautatzean! + Gehitu iruzkin publikoa + Bidali iruzkina + Ez dago Internet konexiorik. Zure mezua zirriborroetan gorde da. + Testu laua + HTML + Markdown + Amaitu saioa kontu honekin + Denak + Babestu aplikazioa + Open Collective-k talde bat erraz sortzea, finantziazioa eskuratzea, eta gardenki kudeatzea errazten du. + Kopiatu esteka + Konektatu + Arrunta + Trinkoa + Kontsola + Ezarri pantaila-modua + Ezarri segurtasun hornitzailea + Eguneratu jarraipen-domeinuak + Jarraipen datu-basea eguneratu da! + http deiak aplikazioak blokeatuta + Blokeatutako deien zerrenda + Bidali + Datu-basea esportatu da! + Nabarmendutako traolak + Iragazi denbora-lerroa etiketekin + Etiketarik ez + Ezkutatu \"ezabatu\" botoia jakinarazpenen fitxan + Erantsi irudi bat URL bat partekatzean + + Inkesta + Inkestak + Sortu inkesta bat + 1. aukera + 2. aukera + %d. aukera + Gutxienez bi aukera behar dira inkesta bat egiteko! + Egina + amaiera: %s + Freskatu inkesta + Eman botoa + Zuk erantzun duzun inkesta bat bukatu da + Zuk bidalitako inkesta bat amaitu da + Pertsonalizatu + Kategoriak + Denbora tartea + Aurreratua + Erakutsi \'berria\' ikurra irakurri gabeko toot-etan + Peertube + Aldatu denbora-lerroa lekuz + Ezkutatu denbora-lerroa + Antolatu denbora-lerroak + Zerrenda behin betiko ezabatuta + Jarraitutako instantzia kenduta + Finkatutako etiketa kenduta + Desegin + Bi fitxa ikusgai mantendu behar dituzu! + Antolatu denbora-lerroak + Denbora-lerro nagusiak ezkutatu besterik ezin dira egin! + BBCode + Beti markatu multimedia mingarri gisa + GNU instantzia + Cachean gordetako mezua + Birbidali etiketak erantzunetan + Sakatu luze multimedia gordetzeko + Lausotu multimedia hunkigarria + Bistaratu denbora-lerroak zerrenda batean + Bistaratu denbora-lerroak + Markatu bot kontuak toot-etan + Kudeatu etiketak + Gogoratu posizioa hasiera debora-lerroan + Historiala + Erreprodukzio-zerrendak + Pantaila-izena + Ez duzu erreprodukzio-zerrendarik. Sakatu \'+\' ikonoa erreprodukzio-zerrenda berria gehitzeko + Pantaila-izen bat eman behar duzu! + Kanala beharrezkoa da erreprodukzio-zerrenda publikoa bada. + Sortu erreprodukzio-zerrenda + Ez dago ezer erreprodukzio-zerrenda honetan oraindik. + berregin + Galeria + Emoji + Eranskailua + Ezabagailua + Testua + Iragazi + Brotxa + Ziur al zaude irudia gorde gabe atera nahi duzula? + Baztertu + Gordetzen… + Irudia ongi gorde da! + Huts egin du irudia gordetzean + Opakutasuna + Gaitu argazki-editorea + Gehitu elementu bat inkestara + Kendu inkestako azken elementua + Mututu elkarrizketa + Desmututu elkarrizketa + Elkarrizketa ez dago jada mutututa! + Elkarrizketa mutututa dago + Ireki aplikazioaren ezaugarriak + Denbora baterako mutututa + Aipatu kontua + Freskatu cachea + Aipatu mezua + Berriak + Orokorra + Eskualdekoa + Artea + Kazetaritza + Aktibismoa + Bideo-jolasak + Teknologia + Helduentzako edukia + Furry + Janaria + Instantziaren logoa + Arazoren bat egon da instantzia eskuragarriak egiaztatzean! + Elkartu Mastodon-era + Aukeratu instantzia bat kategoria bat hautatuz, gero sakatu egiaztatu botoia. + Aukeratu instantzia egiaztatze botoian sakatuz. + %1$s erabiltzaile + Berretsi pasahitza + %1$s eta %2$s onartzen ditut + zerbitzariaren arauak + zerbitzuaren baldintzak + Erregistratu + Instantzia hau gonbidapenen bidez dabil. Zure kontua eskuz onartu behar du administratzaile batek zuk erabili ahal izateko. + Bete eremu guztiak! + Pasahitzak ez datoz bat! + E-maila ez dirudi baliozkoa! + Zure erabiltzaile-izena bakana izango da %1$s instantzian + Baieztapen e-mail bat jasoko duzu + Erabili gutxienez 8 karaktere + Pasahitzak gutxienez 8 karaktere izan behar ditu + Erabiltzaile-izenak hizkiak, zenbakiak eta azpimarrak besterik ezin ditu izan + Kontua sortuta! + Zure kontua sortu da!\n\n + Berretsi e-mail helbidea 48 ordu igaron aurretik.\n\n + Kontura konektatu zaitezke %1$s lehen eremuan idatziz eta Konektatu sakatuz.\n\n + Garrantzitsua: Zure instantziak balioztatzea eskatzen badu, e-mail bat jasoko duzu behin balioztatuta dagonean! + + Gorde mezua zirriborroetan? + Administrazioa + Salaketak + Ez dago salaketarik bistaratzeko! + Berriro konektatu kontua + Aplikazioak huts egin du administrazio ezaugarriak atzitzean. Kontua berriro konektatu behar zenezake ingurune egokia izateko. + Ebatzi gabe + Urrunekoa + Aktiboa + Egiteke + Desgaitua + Isilarazita + Etenda + Baimenak + E-mailaren egoera + Saioaren egoera + Elkartze-data + Azken IP helbidea + Abisatu + Desgaitu + Isilarazi + Jakinarazi erabiltzaileari e-mail bidez + Abisu pertsonalizatua + Erabiltzailea + Moderatzailea + Administratzailea + Baieztatuta + Baieztatu gabe + Salatutako mezuak + Kontua + Ez isilarazi + Ez desgaitu + Eten + Ez eten + Kontua isilarazita dago! + Kontua ez dago jada isilarazita! + Kontua etenda dago! + Kontua ez dago jada etenda! + Kontua desgaituta dago! + Kontua ez dago jada desgaituta! + Kontuari abisua eman zio! + Bistaratu administrazio menua + Bistaratu administrazio ezaugarria mezuetan + Baimendu + Kontua onartu da! + Kontua ukatu da! + Esleitu niri + Kendu esleipena + Markatu ebatzita gisa + Markatu ebatzi gabeko gisa + Edukia hutsik! + Bistaratu Fedilaben ezaugarrien botoia + Aplikazioak audioa grabatzeko baimena behar du + Ahots-mezua + Gaitu erantzun azkarra + Erantzuten ari zaren kontuak agian ez du zure mezua ikusiko! + Desgaituta badago, aplikazioak beti kargatuko ditu azken mezuak + Desgaituta badago, multimedia hunkigarria botoi batekin ezkutatuko da + Gorde multimedia tamaina osoan aurrebistetan luze sakatuz + Gehitu botoi bat goi-eskuin erpinean etiketa, instantzia eta zerrenda guztiak erakusteko + Denbora tarte horretan aplikazioak jakinarazpenak igorriko ditu. Denbora tartea alderantzikatu dezakezu (adib. isildu) eskuineko kontrolarekin. + Bistaratu Fedilab botoi bat profilaren argazkiaren azpian. Aplikazio barneko ezaugarriak atzitzeko lasterbide bat da. + Baimendu denbora-lerroetan zuzenean mezuen azpian erantzutea + Aurrebistak ez dira mozten denbora-lerroetan + Baimendu txertatutako bideoak denbora-lerroetan bertan erreproduzitzea + Baimendu jaso gehiago botoia sakatzean bistaratzen diren mezuak irakurtzeko modua alderantzikatzea + Aukera honek zifratze suite berriak onartzea baimentzen du. Android gailu zaharretan erabilgarria da edo zure instantziara konektatzeko arazoak badituzu. + Peertube bideoentzako besterik ez. Aldatu modua ezin badituzu erreproduzitu. + Etiketa hauek profilen mezuak iragaztea ahalbidetzen dute. Laster-menua erabili beharko duzu hauek ikusteko. + Txertatu lerro saltoa automatikoki aipamenaren ostean lehen hitza letra larriz jartzeko + Ahalbidetu eduki sortzaileei mezuak bere RSS jarioen bidez partekatzea + Idazketa + Gehieneko saiakera kopurua multimedia igotzean + Sortu karpeta berria hemen + Sartu karpetaren izena + Sartu baliozko karpeta izen bat + Karpeta hau badago aurretik.\n Eman beste izen bat karpetarentzat + Hautatu + Direktorio lehenetsia + Karpeta + Sortu karpeta + Bistaratu laster-jakinarazpen bat ekintzaren bat burutu eta gero (bultzada, gogokoa, eta abar)? + Mutututako instantziak esportatu dira! + Gehitu instantzia bat + Esportatu instantziak + Inportatu instantziak + Kraskatze txostenak + Gaitu kraskatze txostenak + Gaituta badago, krakatze txosten bat sortuko da lokalean eta gero partekatu ahal izango duzu. + Fedilab gelditu da :( + Kraskatze txostena e-mail bidez bidali diezadakezu. Konpontzen lagunduko dit :)\n\nEdukia gehitu dezakezu. Eskerrik asko! + Erabili wysiwyg + Gaituz gero, testuei formatua erraz emateko tresnak izango dituzu. + Estatistikak + Mezuak guztira + Bultzada kopurua + Gogoko kopurua + Aipamen kopurua + Jarraitze kopurua + Inkesta kopurua + Erantzun kopurua + Mezu kopurua + Mezuak + Ikusgaitasuna + Multimediadun kopurua + Hunkigarri kopurua + CW duten kopurua + Lehen mezuaren data + Azken mezuaren data + Lehen jakinarazpen data + Azken jakinarazpen data + Maiztasuna + %s mezu eguneko + %s jakinarazpen eguneko + Data-barrutia + Taldeak + Talderik ez! + Desgaitu animatutako emoji pertsonalizatuak + Diagramak + Erakutsi diagramak + Aplikazioa zure datu lokalak biltzen ari da, itxaron mesedez... + Babes-kopia + Mezuen babes-kopia automatikoa + Aukera hau kontu bakoitzeko da. Zure mezuak lokalki datu-basean gordeko dituen zerbitzu bat abiatuko du. Honek estatistikak eta diagramak ahalbidetzen ditu + Jakinarazpenen babes-kopia automatikoa + Aukera hau kontuko da. Automatikoki zure jakinarazpenak datu-base lokalean gordeko dituen zerbitzu bat abiatuko du. Honek estatistikak eta diagramak ahalbidetzen ditu + Salatu kontua + Bidali gonbidapen bat + Zure instantziak ez du kontu berri bat erregistratzea baimentzen! + + boto %d + %d boto + + + Bozkatzaile %d + %d bozkatzaile + + + Aukera bakarra + Hainbat aukera + + + 5 minutu + 30 minutu + Ordu 1 + 6 ordu + Egun 1 + 3 egun + 7 egun + + + Webview + Jario zuzena + + \"%1$s\" nire instantziara elkartzeko, Fedilab deskargatu dezakezu:\n\nF-Droid: %2$s\nGoogle: %3$s\n\nGero ireki beheko esteka Fedilab erabilita eta sortu zure kontua:)\n\n%4$s + Zure inkestak ezin ditu bikoiztutako aukerak izan! + Kontu guztientzat + Datu-basearen cachea + Garbitu zure hasiera denbora-lerroaren cachea + Garbitu cacheko mezuak + Garbitu gogokoak + Fitxategiak cachean + Jakinarazpenak guztira + Ezkutatu menuko elementuak + Fedilabek zuzeneko jakinarazpenak erabiltzen ditu + %2$s gertaera dituzten %1$s kontuentzat + %1$s(r)entzako zuzeneko jakinarazpenak + Zuzeneko jakinarazpenak gaituko dira kontu honentzat. + Garbitu cachea irtetean + Cachea (multimedia, mezuak, barneko nabigatzailearen datuak) automatikoki ezabatuko da aplikaziotik irtetean. + Kontu hau jarraitzeari utzi nahi diozu? + Erakutsi baieztapen elkarrizketa-koadroa jarraitzeari utzi aurretik + Erabili Invidio.us Youtube-ren ordez + Invidio.us Youtube ikusteko ordezko interfaze bat da + Sartu zure ostalari pertsonalizatua edo laga hutsik invidio.us erabiltzeko + Erabili Nitter Twitter-en ordez + Nitter pribatutasuna aintzat duen Twitter interfaze libre bat da. + Sartu zure ostalari pertsonalizatua edo laga hutsik nitter.net erabiltzeko + Ordeztu Instagram Bibliogram-ekin + Bibliogram pribatutasuna aintzat duen Instagram interfaze libre bat da. + Sartu zure ostalari pertsonalizatua edo laga hutsik bibliogram.art erabiltzeko + Replace Reddit with Libreddit + Libreddit is an open source alternative Reddit front-end focused on privacy. + Enter your custom host or leave blank for using libredd.it + Replace Medium links + Replace medium.com links with an open source alternative front-end focused on privacy. + Default: scribe.rip + Replace Wikipedia links + Replace Wikipedia link with an open source alternative front-end focused on privacy. + Default: wikiless.org + Ezkutatu Fedilab jakinarazpen-barra + Egoera barran geratzen den jakinarazpena ezkutatzeko, sakatu begiaren ikonoa eta desmarkatu \"Erakutsi egoera-barran\" + Use a push notifications system for getting notifications in real time. + Zuzeneko jakinarazpenik ez + Live notifications + Jakinarazpenak 15 minutuero jasoko dira. + Gehitu oharrak + Kontuarentzako oharrak + Baimendu irudi handiak tamainaz txikiagotzea kalitatean aldaketa nabarmenik gabe. + Baimendu bideoak konprimitzea kalitatea mantenduz. + Aplikazioa multimedia konprimitzen ari da, honek luze hartu dezake… + Aldatu aplikazioaren ikonoa + Sakatu aplikazioaren ikonoa aldatzeko + Mezua + Mezuaren ikusgaitasuna + Sakatu hemen argazkian gehitzeko + Onartutako formatuak: jpeg, png, gif \n\nGehieneko fitxategiaren tamaina: 15 MB \n\nAlbumek 4 argazki edo bideo bildu itzakete + Igo multimedia + Gehitu azpititulua nahiez gero + Aplikazioak errore mezu oso luze bat jaso du %1$s APItik + Mezuaren aurrebista + Gehitu aipamenak mezu bakoitzean + Elkarrizketa jasotzen + Ordena + Bideoaren izenburua + Elkartu Peertube-ra + 16 urte edo gehiago ditut eta instantzia honen %1$s onartzen ditut + Estekak + Aldatu mezuetako esteken koloreak (URL-ak, aipamenak, etiketak, eta abar) + Bultzaden goiburua + Aldatu mezuen goialdeko pantaila-izenaren kolorea + Aldatu mezuen goialdeko erabiltzaile-izenaren kolorea + Aldatu bultzaden goiburuen kolorea + Bidalketak + Mezuen atzealdearen kolorea denbora-lerroetan + Berrezarri koloreak + Sakatu hemen zure kolore pertsonalizatu guztiak leheneratzeko + Leheneratu + Ikonoak + Denbora-lerroen azpialdeko botoien kolorea + Finkatu etiketa hau + Instantziaren logoa + Editatu profila + Burutu ekintza bat + Itzulpena + Irudiaren aurrebista + Testuaren kolorea + Aldatu mezuen testuaren kolorea + Aplikatu aldaketak + Aplikazioa berrabiarazi behar duzu aldaketak aplikatzeko + Berrabiarazi + Erabili azal pertsonalizatua + Baimendu goian hautatutako azalaren koloreak gainidaztea + Gaiak + Lehenbizi gorde + Azala esportatu da + Azala ongi esportatu da CSV gisa + Aplikatu kolore nagusia egoera barrari + Egoera barraren kolorea + Berrezarri lehenetsitako azal bat + Inportatu azal bat + Sakatu hemen aurretik esportatutako azal bat inportatzeko + Esportatu azala + Sakatu hemen oraingo azala esportatzeko + Errore bat gertatu da azalaren fitxategia hautatzean + Azal hautatzailea + Hautatu aurrez instalatutako azal bat + Azalak + Aplikatu kolore nagusia nabigazio barrari + Nabigazio barraren kolorea + Aplikazioaren edukien atzeko kolorea. + Atzeko kolorea + Interfazearen atal hautatuak nabarmentzen ditu. + Nabarmentze kolorea + Zure aplikazioan gehien erabiliko dena. + Kolore nagusia + Esportatu gogokoak instantziara + Inportatu gogokoak instantziatik + Erabiltzaile kopurua + Mezu kopurua + Instantzia kopurua + Blokeatuta + Amaitzeko %s + Zer dago berri %s bertsioan + Nire kontua jarraitu dezakezu eguneraketetarako + Instantzia hau ez dago eskuragarri https://instances.social gunean + Erakutsi esteka osoa + Partekatu esteka + URL-a arbelera kopiatu da + Ireki beste aplikazio batekin + Egiaztatu birbideratzea + URL honek ez du birbideratzen + %1$s \n\n helbideak \n\n %2$s helbidera birbideratzen du + Aldatu erabiltzaile-agentea + Ezarri erabiltzaile-agente pertsonalizatu bat edo laga hutsik + API deietarako eta barne nabigatzailean erabilitako erabiltzaile-agentea pertsonalizatzeko aukera ematen du. + Kendu UTM parametroak + Aplikazioak automatikoki kenduko ditu URL baten UTM parametroak hau bisitatu aurretik. + Joerak + Joera orain + %d pertsona hitz egiten + Twitter kontuak (Nitter bidez) + Twitter erabiltzaile-izenak zuriunez bereizita + Identitate frogak + Egiaztatutako identitatea + Egiaztatzailea: %1$s (%2$s) + Ezabatu jakinarazpena + Erakutsi aukera gehiago + Pixelfed istorio bat da + Igo multimedia, automatikoki gehituko da zure Pixelfed istoriora. + Multimedia ongi gehitu da zure istoriora! + Ekintza desgaituta + Utzi jarraitzeari + Okerren bat egon da, egiaztatu zure deskargen direktorioa ezarpenetan. + Iragarpenak + Iragarpenik ez! + Gehitu erreakzioa + Erabili zure gogoko nabigatzailea aplikazio barruan. Desmarkatu ezaugarri hau estekak kanpoan irekitzeko. + Bideo cachea MB-tan, zerok cacherik ez esan nahi du. + Watermarks + Automatically add a watermark at the bottom of pictures. The text can be customized for each account. + No distributors found! + You need a distributor for receiving push notifications.\nYou will find more details at %1$s.\n\nYou can also disable push notifications in settings for ignoring that message. + Select a distributor + diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml new file mode 100644 index 000000000..99242481f --- /dev/null +++ b/app/src/main/res/values-fa/strings.xml @@ -0,0 +1,1142 @@ + + + باز کردن منو + بستن منو + درباره + درباره این نمونه + حریم شخصی + حافظه نهان + خروج از حساب + ورود + + بستن + بله + خیر + لغو + بارگیری + بارگیری %1$s + رسانه، ذخیره شد + پرونده: %1$s + گذرواژه + رایانامه + حساب‌ها + بوق‌ها + برچسب‌ها + ذخیره + بازگردانی + بی نتیجه! + نمونه + نمونه: mastodon.social + اکنون با این حساب کار می‌کند: %1$s + افزودن حساب کاربری + از محتوای بوق در حافظه رونوشت گرفته شد + از نشانی بوق در حافظه رونوشت گرفته شد + تغییر دادن + انتخاب تصویر... + پاک‌سازی + دوربین + حذف همه + ترجمه این بوق. + زمان‌بندی + اندازه متن و آیکون + تغییر اندازه متن فعلی: + تغییر اندازه آیکون فعلی: + بعدی + قبلی + باز کردن با + تایید + رسانه + هم‌رسانی با + هم‌رسانی با Fedilab + پاسخ‌ها + نام کاربری + پیش‌نویس‌ها + پسندها + دنبال‌کننده‌های جدید + اشاره‌ها + تقویت‌ها + نمایش تقویت‌ها + نمایش پاسخ‌ها + باز کردن در مرورگر + ترجمه + لطفا چند قانیه پیش از انجام این کار صبر کنید. + + خانه + خط زمانی محلی + خط زمانی سراسری + گزینه‌ها + پسندها + ارتباطات + کاربران بی صدا شده + کاربران مسدود شده + اعلان‌ها + درخواست پیگیری + تنظیمات + حذف یک حساب + حذف حساب %1$s از برنامه؟ + فرستادن یک رایانامه + برای تغییر مسیر، روی آن کلیک کنید + ناموفق! + بوق‌های زمان‌بندی شده + اطلاعات زیر ممکن است نمایه کاربر را کامل منعکس نکند. + درج شکلک + The app did not collect custom emojis for the moment. + Push notifications + آیا مطمئن هستید که می‌خواهید خارج شوید؟ + Are you sure you want to logout @%1$s@%2$s? + + بوقی برای نمایش نیست + داستانی برای نمایش موجود نیست + داستان‌ها + تقویت شده توسط %1$s + این بوق به علاقه‌مندی‌ها اضافه شود؟ + این بوق از علاقه‌مندی‌ها حذف شود؟ + این بوق تقویت شود؟ + تقویت این بوق حذف شود؟ + این بوق سنجاق شود؟ + سنجاق این بوق برداشته شود؟ + بی‌صدا + مسدود کردن + گزارش + حذف + رونوشت‌ + به‌اشتراک‌گذاری + اشاره + بی‌صدا کردن زمان‌دار + حذف و بازنویسی + + این حساب بی‌صدا شود؟ + این حساب مسدود شود؟ + این بوق گزارش شود؟ + این دامنه مسدود شود؟ + این حساب از حالت بی‌صدا خارج شود؟ + مسدودیت این حساب رفع شود؟ + + + با اعلان + بی اعلان + + + این بوق پاک شود؟ + این بوق پاک و بازنویسی شود؟ + + نشانک‌ها + افزودن به نشانک‌ها + حذف نشانک + نشانکی برای نمایش وجود ندارد + Status has been added to bookmarks! + مطلب از نشانک‌ها حذف شد! + + %d ث + %d د + %d س + %d ر + + %d second + %d ثانیه + + + %d minute + %d دقیقه + + + %d hour + %d ساعت + + + %d day + %d روز + + + هشدار + به چه چیز فکر می‌کنی؟ + بوق! + QUEET! + cw + بوقی بنویسید + به یک بوق پاسخ دهید + Write a queet + Reply to a queet + رسانه‌ای را انتخاب کنید + An error occurred while selecting the media! + Remove this media? + Your toot is empty! + Visibility of the toot + Visibility of the toots by default: + The toot has been sent! + You are replying to this toot: + Sensitive content? + + Post to public timelines + Do not post to public timelines + Post to followers only + Post to mentioned users only + + No drafts! + Choose a toot + Choose an account + Select some accounts + Delete draft? + Tap on the button to display the original toot + Describe for the visually impaired + + No description available! + + Release %1$s + Developer: + License: + GNU GPL V3 + Source code: + Translation of toots: + Search instances: + Icon designer: + + Conversation + + No account to display + No follow request + Toots \n %1$s + Following \n %1$s + Followers \n %1$s + Pinned \n %d + Authorize + Reject + + No scheduled toots to display! + Write a toot and then choose Schedule from the top menu. + Delete scheduled toot? + Media: %d + The toot has been scheduled! + The scheduled date must be greater than the current hour! + Battery saver is enabled! It might not work as expected. + + The time for muting should be greater than one minute. + %1$s has been muted until %2$s.\n You can unmute this account from their profile page. + %1$s is muted until %2$s.\n Tap here to unmute the account. + + No notification to display + mentioned you + wrote a new message + boosted your status + favourited your status + followed you + asked to follow you + + and another notification + and %d other notifications + + + %d like + %d likes + + Delete a notification? + Delete all notifications? + The notification has been deleted! + All notifications have been deleted! + + Following + Followers + Pinned + + Unable to get client id! + Unable to connect to instance domain! + No Internet connection! + The account was blocked! + The account is no longer blocked! + The account was muted! + The account is no longer muted! + The account was followed! + The account is no longer followed! + The toot was boosted! + The toot is no longer boosted! + The toot was added to your favourites! + The toot was removed from your favourites! + The toot was reported! + The toot was deleted! + The toot was pinned! + The toot was unpinned! + Oops ! An error occurred! + An error occurred! The instance did not return an authorisation code! + The instance domain does not seem to be valid! + An error occurred while switching between accounts! + An error occurred while searching! + The profile data have been saved! + No action can be taken + The media has been saved! + An error occurred while translating! + Translations are disabled in settings + Draft saved! + Are you sure this instance allows this number of characters? Usually, this value is close to 500 characters. + Visibility of the toots has been changed for the account %1$s + + Number of toots per load + Always + WIFI + Ask + Load the media + Load the pictures + Show more… + Show less… + Sensitive content + Disable GIF avatars + Path: + Save drafts automatically + Add URL of media in toots + Notify when someone follows you + Notify when someone boosts your status + Notify when someone favourites your status + Notify when someone mentions you + Notify when a poll ended + Notify for new posts + Show confirmation dialog before boosting + Show confirmation dialog before adding to favourites + Notify in WIFI only + Notify? + Silent Notifications + NSFW view timeout (seconds, 0 means off) + Media Description timeout (seconds, 0 means off) + Edit profile + Custom sharing + Your custom sharing URL… + Bio… + Lock account + Save changes + Choose a header picture + Fit preview images + Automatically split toots in replies when chars are over: + You have reached the 160 characters allowed! + You have reached the 30 characters allowed! + Between + and + The time must be greater than %1$s + The time must be lower than %1$s + Start time + End time + Use the built-in browser + Custom tabs + Enable Javascript + Automatically expand cw + Allow third-party cookies + Your API key, you can leave blank for Yandex + + Dark + Light + Black + + Set LED colour: + + Blue + Cyan + Magenta + Green + Red + Yellow + White + + Follow + Unblock + Mute + Unmute + Request sent + Follows you + Search + First letter in capital for replies + Resize pictures + Resize videos + + Push notifications + Please, confirm push notifications that you want to receive. + You can enable or disable these notifications later in settings (Notifications tab). + + + Clear cache + There are %1$s of data in cache.\n\nWould you like to delete them? + Mb + Cache was cleared! %1$s were released + + Title + Title… + Description + Keywords + Keywords… + + Synchronize + Filter + Your toots + Your notifications + Public + Unlisted + Private + Direct + Some keywords… + Show media + Show pinned + No matching result found! + Backup toots for %1$s + %1$s new toots have been imported + %1$s new notifications have been imported + + Dates descending + Dates ascending + + + No + Only + Both + + No toots were found in database. Please, use the synchronize button from the menu to retrieve them. + + Recorded data + Only basic information from accounts are stored on the device. + These data are strictly confidential and can only be used by the application. + Deleting the application immediately removes these data.\n + ⚠ Login and passwords are never stored. They are only used during a secure authentication (SSL) with an instance. + + Permissions: + - ACCESS_NETWORK_STATE: Used to detect if the device is connected to a WIFI network.\n + - INTERNET: Used for queries to an instance.\n + - WRITE_EXTERNAL_STORAGE: Used to store media or to move the app on a SD card.\n + - READ_EXTERNAL_STORAGE: Used to add media to toots.\n + - BOOT_COMPLETED: Used to start the notification service.\n + - WAKE_LOCK: Used during the notification service. + + API permissions: + - Read: Read data.\n + - Write: Post statuses and upload media for statuses.\n + - Follow: Follow, unfollow, block, unblock.\n\n + ⚠ These actions are carried out only when user requests them. + + Tracking and Libraries + The application does not use tracking tools (audience measurement, error reporting, etc.) and does not contain any advertising.\n\n + The use of libraries is minimized: \n + - Glide: To manage media\n + - Android-Job: To manage services\n + - PhotoView: To manage images\n + + Translation of toots + The application offers the ability to translate toots using the locale of the device and the Yandex API.\n + Yandex has its proper privacy-policy which can be found here: https://yandex.ru/legal/confidential/?lang=en + + Thank you to: + + Filter out by regular expressions + Search + Delete + Fetch more toots… + + Lists + Are you sure you want to permanently delete this list? + There is nothing in this list yet. When members of this list post new statuses, they will appear here. + Add to list + Add list + Delete list + Edit list + New list title + The account was added to the list! + You don\'t have any lists yet! + + %1$s has moved to %2$s + Authentication does not work? + Here are some checks that might help:\n\n + - Check there is no spelling mistakes in the instance name\n\n + - Check that your instance is not down\n\n + - If you use the two-factor authentication (2FA), please use the link at the bottom (once the instance name is filled)\n\n + - You can also use this link without using the 2FA\n\n + - If it still does not work, please raise an issue on FramaGit at https://framagit.org/tom79/fedilab/issues + + Media has been loaded. Tap here to display it. + This action can be quite long. You will be notified when it will be finished. + Still running, please wait… + Export statuses + Export statuses for %1$s + %1$s toots out of %2$s have been exported. + Something went wrong when exporting data for %1$s + Something went wrong when exporting data! + Something went wrong when importing data! + + Proxy + Enable proxy? + Host + Port + Login + Password + Add toot details when sharing + Support the app on Liberapay + There is an error in the regular expression! + No timelines was found on this instance! + Delete this instance? + Translate in + Follow instance + You already follow this instance! + The instance is followed! + Partnerships + Information + Hide boosts from %s + Feature on profile + Show boosts from %s + Don\'t feature on profile + The account is now featured on profile + The account is no longer featured on profile + Boosts are now shown! + Boosts are now hidden! + Direct message + Filters + No filters to display. You can create one by tapping on the \"+\" button. + Keyword or phrase + Home timeline + Public timelines + Notifications + Conversations + Will be matched regardless of casing in text or content warning of a toot + Drop instead of hide + Filtered toots will disappear irreversibly, even if filter is later removed + When the keyword or phrase is alphanumeric only, it will only be applied if it matches the whole word + Whole word + Filter contexts + One or multiple contexts where the filter should apply + Expire after + Delete filter? + Update filter + Create filter + Whom to follow + There is no accounts listed for the moment! + Follow + Select all + Unselect all + %s is followed! + Creating the list %s + Adding accounts to the list + Accounts were added to the list + Adding accounts to the list + You have not created a list yet. Tap on the \"+\" button to add a new one. + Who to follow + Trunk API + Account(s) can\'t be followed + Fetching remote account + Automatically expand hidden media + New follow + New Boost + New Favourite + New Mention + Poll Ended + New Toot + Toots Backup + New posts + Media Download + Change notification sound + Select Tone + Enable time slot + How To Videos + Fetching remote thread! + No blocked domains! + Unblock domain + Are you sure to unblock %s? + Are you sure to block %s?\n\nYou will not see any content from that domain in any public timeline or in your notifications. Your followers from that domain will be removed. + Blocked domains + Block domain + The domain is blocked + The domain is no longer blocked! + Fetching remote status + Comment + Peertube instance + Be the first to leave a comment on this video with the top right button! + %s views + Duration: %s + Add an instance + Comments are not enabled on this video! + Pick up a resolution + Peertube favourites + The video has been added to bookmarks! + این ویدیو از نشانک‌ها پاک شده است! + There is no Peertube videos in your favourites! + Channel + Videos + Channels + Use Emoji One + Information + Display previews in all toots + New UX/UI designer + Display video previews + The account id has been copied in the clipboard! + Change the language + Default language + Truncate long toots + Truncate toots over \'x\' lines. Zero means disabled. + Display more + Display less + Manage tags + The tag already exists! + The tag has been stored! + The tag has been changed! + The tag has been removed! + Schedule boost + The boost is scheduled! + No scheduled boost to display! + Schedule boost.]]> + Art timeline + Open menu + Go back + Logo of the application + Profile picture + Profile banner + Contact admin of the instance + Add new + MastoHost logo + Emoji picker + Refresh + Expand the conversation + Remove an account + Remove the blocked domain + Custom emoji picker + Play video + New toot + Image of the card + Hide media + Favicon + Add description for media (for the visually impaired) + + Never + 30 minutes + 1 hour + 6 hours + 12 hours + 1 day + 1 week + + In this field, you need to write your instance host name.\nFor example, if you created your account on https://mastodon.social\nJust write mastodon.social (without https://)\n + You can start writing first letters and names will be suggested.\n\n + ⚠ The Login button will only work if the instance name is valid and the instance is up! + + More information + + Languages + Media only + Show NSFW + Crowdin translations + Crowdin manager + Translation of the application + About Crowdin + Bot + Pixelfed instance + Mastodon instance + Any of these + All of these + None of these + Any of these words (space-separated) + All these words (space-separated) + Add some words to filter (space-separated) + Change column name + Misskey instance + No app supporting this link is installed on your device. + Subscriptions + Overview + Trending + Recently added + Local + Upload + Reply + Delete a comment + Are you sure to delete this comment? + Full screen video + Mode for videos + Select the file to upload + My videos + Title + License + Category + Language + This video contains mature or explicit content + Enable video comments + Update video + Description + The video has been updated! + Upload cancelled! + The video has been uploaded! + Uploading, please wait… + Tap here to edit the video data. + Delete video + Are you sure to delete this video? + Display NSFW videos + No videos to display! + Leave a comment + Share + Choose a schedule mode + From device + From server + Toots (Server) + Toots (Device) + Modify + Display new toots above the \"Fetch more\" button + Timelines + Interface + Contacts + %1$s commented your video %2$s]]> + %1$s is following your channel %2$s]]> + %1$s is following your account]]> + %1$s has been published]]> + %1$s succeeded]]> + %1$s failed]]> + %1$s published a new video: %2$s]]> + %1$s has been blacklisted]]> + %1$s has been unblacklisted]]> + Export data + Import Data + Select the file to import + An error occurred when selecting the backup file! + Add a public comment + Send comment + There is no Internet connection. Your message has been stored in drafts. + Plain text + HTML + Markdown + Logout account + All + Support the app + Open Collective enables groups to quickly set up a collective, raise funds and manage them transparently. + Copy link + Connect + Normal + Compact + Console + Set display mode + Patch the Security Provider + Update tracking domains + The tracking data base has been updated! + http calls blocked by the application + List of blocked calls + Submit + The data base has been exported! + Featured hashtags + Filter timeline with tags + No tags + Hide the \"delete\" button in the notification tab + Attach an image when sharing a URL + + Poll + Polls + Create a poll + Choice 1 + Choice 2 + Choice %d + You need two choices at least for the poll! + Done + end at %s + Refresh poll + Vote + A poll you have voted in has ended + A poll you tooted has ended + Customize + Categories + Time slot + Advanced + Display \'new\' badge on unread toots + Peertube + Move timeline + Hide timeline + Reorder timelines + List permanently deleted + Followed instance removed + Pinned tag removed + Undo + You need to keep two visible tabs! + Reorder timelines + Main timelines can only be hidden! + BBCode + Always mark media as sensitive + GNU instance + Cached status + Forward tags in replies + Long press to store media + Blur sensitive media + Display timelines in a list + Display timelines + Mark bot accounts in toots + Manage tags + Remember the position in Home timeline + History + Playlists + Display name + You don\'t have any playlists. Tap on the \"+\" icon to add a new playlist + You must provide a display name! + The channel is required when the playlist is public. + Create a playlist + There is nothing in this playlist yet. + redo + Gallery + Emoji + Sticker + Eraser + Text + Filter + Brush + Are you sure you want to exit without saving the image? + Discard + Saving… + Image Saved Successfully! + Failed to save Image + Opacity + Enable photo editor + Add a poll item + Remove last poll item + Mute conversation + Unmute conversation + The conversation is no longer muted! + The conversation is muted + Open application features + Timed mute + Mention the account + Refresh cache + Mention the status + News + General + Regional + Art + Journalism + Activism + Gaming + Technology + Adult content + Furry + Food + Logo of the instance + Something went wrong when checking available instances! + Join Mastodon + Choose an instance by picking up a category, then tap on a check button. + Choose an instance by tapping on a check button. + %1$s users + Confirm password + I agree to %1$s and %2$s + server rules + terms of service + Sign up + This instance works with invitations. Your account will need to be manually approved by an administrator before being usable. + Please, fill all the fields! + Passwords don\'t match! + The email doesn\'t seem to be valid! + Your username will be unique on %1$s + You will be sent a confirmation e-mail + Use at least 8 characters + Password should contain at least 8 characters + Username should only contain letters, numbers and underscores + Account created! + Your account has been created!\n\n + Think to validate your email within the 48 next hours.\n\n + You can now connect your account by writing %1$s in the first field and click on Connect.\n\n + Important: If your instance required validation, you will receive an email once it is validated! + + Save the message in drafts? + Administration + Reports + No reports to display! + Reconnect the account + The application failed to access the admin features. You might need to reconnect the account for having the correct scope. + Unresolved + Remote + Active + Pending + Disabled + Silenced + Suspended + Permissions + Email status + Login status + Joined + Most recent IP + Warn + Disable + Silence + Notify the user per e-mail + Custom warning + User + Moderator + Administrator + Confirmed + Not confirmed + Reported statuses + Account + Undo silence + Undo disable + Suspend + Undo suspend + The account is silenced! + The account is no longer silenced! + The account is suspended! + The account is no longer suspended! + The account is disabled! + The account is no longer disabled! + The account has been warned! + Display the admin menu + Display the admin feature in statuses + Allow + The account is approved! + The account is rejected! + Assign to me + Unassign + Mark as resolved + Mark as unresolved + Empty content! + Display Fedilab features button + The application needs to access audio recording + Voice message + Enable quick reply + The account you are replying might not see your message! + If disabled, the app will always load last statuses + If disabled, sensitive media will be hidden with a button + Store media in full size with a long press on previews + Add an ellipse button at the top right for listing all tags/instances/lists + During the time slot, the app will send notifications. You can reverse (ie: silent) this time slot with the right spinner. + Display a Fedilab button below profile picture. It is a shortcut for accessing in-app features. + Allow to reply directly in timelines below statuses + Previews will not be cropped in timelines + Allow to play embedded videos directly in timelines + Allow to reverse the way to read statuses that are displayed once tapping the fetch more button + This option allows to support recent cipher suites. It is useful for older Android devices or if you cannot connect to your instance. + Exclusively for Peertube videos. Switch this mode if you cannot play them. + These tags will allow to filter statuses from profiles. You will have to use the context menu for seeing them. + Automatically insert a line break after the mention to capitalize the first letter + Allow content creators to share statuses to their RSS feeds + Compose + Maximum retry times when uploading media + Create a new Folder here + Enter the folder name + Please enter a valid folder name + This folder already exists.\n Please provide another name for the folder + Select + Default Directory + Folder + Create folder + Display a toast message after an action has been completed (boost, fav, etc.)? + Muted instances have been exported! + Add an instance + Export instances + Import instances + Crash reports + Enable crash reports + If enabled, a crash report will be created locally and then you will be able to share it. + Fedilab has stopped :( + You can send me by email the crash report. It will help to fix it :)\n\nYou can add additional content. Thank you! + Use the wysiwyg + When enabled, you will be able to format your text easily with tools. + Statistics + Total statuses + Number of boosts + Number of favourites + Number of mentions + Number of follows + Number of polls + Number of replies + Number of statuses + Statuses + Visibility + Number with media + Number with sensitive media + Number with CW + First status date + Last status date + First notification date + Last notification date + Frequency + %s statuses per day + %s notifications per day + Date range + Groups + No groups! + Disable custom animated emojis + Charts + Display charts + The application collects your local data, please wait… + Backup + Auto backup statuses + This option is per account. It will launch a service that will automatically store your statuses locally in the database. That allows to get statistics and charts + Auto backup notifications + This option is per account. It will launch a service that will automatically store your notifications locally in the database. That allows to get statistics and charts + Report account + Send an invitation + Your instance does not allow to register a new account! + + %d vote + %d votes + + + %d voter + %d voters + + + Single choice + Multiple choices + + + 5 minutes + 30 minutes + 1 hour + 6 hours + 1 day + 3 days + 7 days + + + Webview + Direct stream + + For joining my instance \"%1$s\", you can download Fedilab:\n\nF-Droid: %2$s\nGoogle: %3$s\n\nThen open the link below with Fedilab and create your account :)\n\n%4$s + + Your poll can\'t have duplicated options! + For all accounts + Database cache + Clear your home timeline cache + Clear your cached statuses + Clear your bookmarks + Files in cache + Total notifications + Hide menu items + Fedilab is running live notifications + For %1$s accounts with %2$s events + Live notifications for %1$s + Live notifications will be enabled for this account. + Clear cache when leaving + The cache (media, cached messages, data from the built-in browser) will be automatically cleared when leaving the application. + Do you want to unfollow this account? + Show confirmation dialog before unfollowing + Replace Youtube with Invidio.us + Invidious is an alternative front-end to YouTube + Enter your custom host or leave blank for using invidious.snopyta.org + Replace Twitter with Nitter + Nitter is an open source alternative Twitter front-end focused on privacy. + Enter your custom host or leave blank for using nitter.net + Replace Instagram with Bibliogram + Bibliogram is an open source alternative Instagram front-end focused on privacy. + Enter your custom host or leave blank for using bibliogram.art + Replace Reddit with Libreddit + Libreddit is an open source alternative Reddit front-end focused on privacy. + Enter your custom host or leave blank for using libredd.it + Replace Medium links + Replace medium.com links with an open source alternative front-end focused on privacy. + Default: scribe.rip + Replace Wikipedia links + Replace Wikipedia link with an open source alternative front-end focused on privacy. + Default: wikiless.org + Hide Fedilab notification bar + For hiding the remaining notification in the status bar, tap on the eye icon button then uncheck: \"Display in status bar\" + Use a push notifications system for getting notifications in real time. + No live notifications + Live notifications + Notifications will be fetched every 15 minutes. + Add notes + Notes for the account + Allow to compress large photos into smaller sized photos with very less or negligible loss in quality of the image. + Allow compressing videos while maintaining their quality. + The app is compressing the media, it can be quite long… + Change app icon + Tap to change the app icon + Post + Visibility of the post + Tap here to add photos + Accepted Formats: jpeg, png, gif \n\nMax File Size: 15 MB \n\nAlbums can contain up to 4 photos or videos + Upload media + Add an optional caption + The app received a very long error message from the API %1$s + پیش‌نمایش پیام + افزودن اشاره‌ها در هر پیام + Fetching conversation + Order by + Title for the video + Join Peertube + I am at least 16 years old and agree to the %1$s of this instance + Links + Change the color of links (URLs, mentions, tags, etc.) in messages + Reblogs header + Change the color of display name at the top of messages + Change the color of the user name at the top of messages + Change the color of the header for reblogs + Posts + Background color of posts in timelines + Reset colors + Tap here to reset all your custom colors + Reset + Icons + Color of bottom icons in timelines + Pin this tag + Logo of the instance + Edit profile + Make an action + Translation + Image preview + Text color + Change the text color in pots + Apply changes + You need to restart the application to apply changes + Restart + Use a custom theme + Allow to override colors of the selected theme above + Theming + Store before + The theme was exported + The theme has been successfully exported in CSV + Apply the primary color to the status bar + Status bar color + Restore a default theme + Import a theme + Tap here to import a theme from a previous export + Export the theme + Tap here to export the current theme + An error occurred when selecting the theme file + Theme Picker + Select a pre-installed theme + Themes + Apply the primary color to the navigation bar + Navigation bar color + The underlying color of the app’s content. + Background color + Accents select parts of the UI. + Accent color + Displayed most frequently across your app. + Primary color + Export bookmarks to the instance + Import bookmarks from the instance + User count + Status count + Instance count + Blocked + End in %s + What\'s new in %s + You can follow my account for updates + This instance is not available on https://instances.social + Display full link + Share link + The URL has been copied to the clipboard + Open with another app + Check redirect + This URL does not redirect + %1$s \n\nredirects to\n\n %2$s + Change the user agent + Set a custom user agent or leave blank + Allows to customize the user agent used for api calls or with the built-in browser. + Remove UTM parameters + The app will automatically remove UTM parameters from URLs before visiting a link. + Trends + Trending now + %d people talking + Twitter accounts (via Nitter) + Twitter usernames space separated + Identity proofs + هویت تایید شده + تایید شده توسط %1$s (%2$s) + Delete the notification + Display more options + It is a Pixelfed story + Upload a media, it will be automatically added to your Pixelfed story. + Media successfully added to your story! + Action disabled + Unfollow + Something went wrong, please check your download directory in settings. + Announcements + No announcements! + Add a reaction + Use your favourite browser inside the app. Uncheck this feature to open links externally. + Video cache in MB, zero means no cache. + Watermarks + Automatically add a watermark at the bottom of pictures. The text can be customized for each account. + No distributors found! + You need a distributor for receiving push notifications.\nYou will find more details at %1$s.\n\nYou can also disable push notifications in settings for ignoring that message. + Select a distributor + diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml new file mode 100644 index 000000000..af6470046 --- /dev/null +++ b/app/src/main/res/values-fr/strings.xml @@ -0,0 +1,1139 @@ + + + Ouvrir le menu + Fermer le menu + À propos + À propos de l’instance + Confidentialité + Cache + Déconnexion + Connexion + + Fermer + Oui + Non + Annuler + Télécharger + Télécharger %1$s + Enregistrement terminé + Fichier : %1$s + Mot de passe + Email + Comptes + Pouets + Étiquettes + Sauvegarder + Restaurer + Aucun résultat ! + Instance + Instance : mastodon.social + Utilisation du compte %1$s + Ajouter un compte + Le contenu du pouet a été copié dans le presse-papier + L\'URL du pouet a été copiée dans le presse-papier + Changer + Sélectionnez une photo… + Nettoyer + Appareil photo + Tout effacer + Traduire ce pouet. + Programmer + Taille du texte et des icônes + Modifier la taille du texte : + Modifier la taille des icônes : + Suivant + Précédent + Ouvrir avec + Valider + Médias + Partager avec + Partagé via Fedilab + Réponses + Nom d’utilisateur + Brouillons + Favoris + Nouveaux⋅elles abonné⋅e⋅s + Mentions + Partages + Afficher les partages + Afficher les réponses + Ouvrir dans le navigateur + Traduire + Veuillez patienter quelques secondes avant de faire cette action. + + Accueil + Fil public local + Fil public global + Options + Favoris + Communication + Utilisateurs en sourdine + Utilisateurs bloqués + Notifications + Demandes d’abonnements + Paramètres + Supprimer un compte + Supprimer le compte %1$s de l’application ? + Envoyer un Email + Cliquer sur le chemin pour le changer + Erreur ! + Pouets programmés + Les données ci-dessous peuvent ne pas refléter ce profil dans sa totalité. + Insérer un émoji + L’application n’a pas encore collecté d’emojis personnalisés. + Notifications poussées + Voulez-vous vraiment vous déconnecter? + Voulez-vous vraiment déconnecter le compte @%1$s@%2$s? + + Aucun pouet à afficher ! + Aucune histoire à afficher + Histoires + Partagé par %1$s + Ajouter ce pouet aux favoris ? + Supprimer ce pouet des favoris ? + Partager ce pouet ? + Supprimer ce pouet des partages ? + Épingler ce pouet ? + Désépingler ce pouet ? + Ignorer + Bloquer + Signaler + Supprimer + Copier + Partager + Mentionner + Temporairement muet + Supprimer & réécrire + + Masquer ce compte ? + Bloquer ce compte ? + Signaler ce pouet ? + Désirez-vous bloquer ce domaine ? + Ne plus masquer ce compte ? + Débloquer ce compte ? + + + Notifier + Silencier + + + Supprimer ce pouet ? + Supprimer & réécrire ce pouet ? + + Signets + Ajouter aux signets + Supprimer le signet + Aucun signet à afficher + Le pouet a été ajouté aux signets ! + Le pouet a été supprimé des signets ! + + %d s + %d min + %d h + %d j + + %d seconde + %d secondes + + + %d minute + %d minutes + + + %d heure + %d heures + + + %d jour + %d jours + + + Avertissement + Qu’avez-vous en tête ? + POUET ! + QUEET! + cw + Écrire un pouet + Répondre à un pouet + Rédiger un queet + Répondre à un queet + Sélectionnez un média + Une erreur s’est produite lors de la sélection du média ! + Supprimer le média ? + Votre pouet est vide ! + Visibilité du pouet + Visibilité des pouets par défaut : + Le pouet a été envoyé ! + Vous répondez à ce pouet : + Contenu sensible ? + + Afficher dans les fils publics + Ne pas afficher dans les fils publics + N\'afficher que pour vos abonné·e·s + N\'afficher que pour les personnes mentionnées + + Aucun brouillon ! + Choisissez un pouet + Choisissez un compte + Sélectionnez des comptes + Supprimer le brouillon ? + Cliquer sur le bouton pour afficher le pouet d’origine + Décrire pour les malvoyants + + Aucune description ! + + Version %1$s + Développeur : + Licence : + GNU GPL V3 + Code source : + Traduction des pouets : + Recherche d’instances : + Concepteur de l’icône : + + Conversation + + Aucun compte à afficher + Aucune demande d’abonnement + Pouets \n %1$s + Abonnements \n %1$s + Abonné·e·s \n %1$s + Épinglés %d + Autoriser + Rejeter + + Aucun pouet programmé à afficher ! + Rédigez un pouet, puis choisissez Programmer dans le menu du haut. + Supprimer le pouet programmé ? + Média(s): %d + Le pouet a été programmé ! + La date doit être supérieure à l’heure actuelle ! + L’économiseur de batterie est activé ! Il se peut que cela ne fonctionne pas comme prévu. + + Le délai pour rendre muet doit être supérieur à une minute. + %1$s est muet jusqu’au %2$s.\n Vous pouvez annuler cette action en vous rendant sur son profil. + %1$s est muet jusqu’au %2$s.\n Cliquez ici pour annuler. + + Aucune notification à afficher + vous a mentionné + a écrit un nouveau message + a partagé votre pouet + a ajouté votre pouet à ses favoris + vous suit + a demandé à vous suivre + + et %d autre notification + et %d autres notifications + + + %d aime + %d aimes + + Supprimer une notification ? + Supprimer toutes les notifications ? + La notification a été supprimée! + Toutes les notifications ont été supprimées ! + + Suit + Abonné·e·s + Épinglé + + Impossible d’obtenir l’id du client ! + Impossible de se connecter au domaine de l\'instance ! + Aucune connexion Internet ! + Le compte a été bloqué ! + Le compte n\'est plus bloqué ! + Le compte a été masqué ! + Le compte n\'est plus masqué ! + Le compte est suivi ! + Le compte n\'est plus suivi ! + Le pouet a été partagé ! + Le pouet a été supprimé des partages ! + Le pouet a été ajouté aux favoris ! + Le pouet a été supprimé des favoris ! + Le pouet a été signalé ! + Le pouet a été supprimé ! + Le pouet a été épinglé ! + Le pouet a été désépinglé ! + Oups ! Une erreur s’est produite ! + Une erreur s’est produite ! L’instance n’a retourné aucun code d\autorisation ! + Le nom de l’instance ne semble pas être valide ! + Une erreur s’est produite pendant le chargement du compte ! + Une erreur s’est produite lors de la recherche ! + Les données du profil ont été sauvegardées ! + Aucune action ne peut être réalisée + Le média a été enregistré ! + Une erreur est survenue lors de la traduction ! + Les traductions sont désactivées dans les paramètres + Brouillon enregistré ! + Êtes-vous sûr que cette instance autorise ce nombre de caractères ? Habituellement, cette valeur est proche de 500 caractères. + La visibilité des pouets a été changée pour le compte %1$s + + Nombre de pouets par chargement + Toujours + WIFI + Demander + Charger les médias + Charger les images + Afficher le contenu ? + Afficher moins… + Charger les images sensibles + Désactiver l’animation des avatars + Destination : + Enregistrer les brouillons automatiquement + Ajouter l’URL des médias dans les pouets + Notifier lorsque quelqu’un me suit + Notifier lorsque quelqu’un partage mes pouets + Notifier lorsque quelqu’un ajoute mes pouets à ses favoris + Notifier lorsque quelqu’un me mentionne + Me notifier lorsqu’un sondage est terminé + Notifier des nouveaux messages + Confirmer avant de partager + Confirmer avant d’ajouter aux favoris + Notifier en WIFI seulement + Notifier ? + Utiliser le vibreur + Délai d’affichage NSFW (en secondes, 0 signifie aucun délai) + Délai de l\'affichage de la description des médias (en secondes, 0 signifie désactivé) + Modifier le profil + Partage personnalisé + Votre URL de partage personnalisé… + Présentation… + Verrouiller le compte + Enregistrer les modifications + Choisissez une image d’entête + Voir l’aperçu d’image en entier + Fractionnement automatique au-delà de 500 caractères + Vous avez atteint les 160 caractères autorisés ! + Vous avez atteint les 30 caractères autorisés ! + Entre + et + L’horaire doit être plus grand que %1$s + L’horaire doit être plus petit que %1$s + Heure de début + Heure de fin + Utiliser le navigateur intégré + Onglets personnalisés + Activer Javascript + Afficher automatiquement le contenu masqué + Autoriser les cookies tiers + Votre clé API, vous pouvez laisser vide pour Yandex + + Sombre + Clair + Noir + + Couleur de la LED : + + Bleu + Cyan + Magenta + Vert + Rouge + Jaune + Blanc + + Suivre + Débloquer + Masquer + Afficher + Demande envoyée + Vous suit + Recherche + Première lettre en majuscule pour les réponses + Redimensionner les images + Redimensionner les vidéos + + Notifications poussées + Veuillez confirmer les notifications que vous souhaitez recevoir. + Vous pourrez les activer ou les désactiver plus tard dans les paramètres (onglet Notifications). + + + Nettoyage du cache + Il y a %1$s de données en cache.\n\nSouhaitez-vous les supprimer ? + Mo + Le cache a été nettoyé ! %1$s ont été libérés + + Titre + Titre… + Description + Mots clés + Mots clés… + + Synchroniser + Filtrer + Mes pouets + Vos notifications + Public + Non-listé + Privé + Direct + Quelques mots-clés… + Pouets avec médias + Pouets épinglés + Aucun résultat avec ce filtre ! + Sauvegarde des pouets pour %1$s + %1$s nouveaux pouets ont été importés + %1$s nouvelles notifications ont été importées + + Dates descendantes + Dates ascendantes + + + Non + Seulement + Les deux + + Aucun pouet trouvé en base de données. Veuillez utiliser le bouton synchroniser du menu pour les importer. + + Données enregistrées + Seules les informations de base des comptes connectés sont enregistrées sur l’appareil. + Ces données sont strictement confidentielles et ne sont utilisables que par l’application. La suppression de l’application entraîne immédiatement la suppression de ces données.\n + ⚠ Les identifiants et les mots de passe ne sont jamais enregistrés, ils ne sont utilisés que lors de l’authentification sécurisée (SSL) vers l’instance. + + Autorisations de l’application : + - ACCESS_NETWORK_STATE : Utilisée pour savoir si l’appareil est connecté au WIFI.\n + - INTERNET : Utilisée pour les requêtes vers l’instance.\n + - WRITE_EXTERNAL_STORAGE : Utilisée pour télécharger les médias / déplacer sur la carte SD.\n + - READ_EXTERNAL_STORAGE : Utilisée pour ajouter des médias aux pouets.\n + - BOOT_COMPLETED : Utilisée pour lancer le service de notifications quand l’appareil démarre.\n + - WAKE_LOCK : Utilisée lors du service de notifications. + + Autorisations de l’API : + - Read : Lire les données du compte.\n + - Write : Envoyer des messages et attacher des médias aux messages.\n + - Follow : S’abonner, se désabonner, bloquer, débloquer.\n\n + ⚠ Ces actions ne sont réalisées qu’à la demande de l’utilisateur·rice. + + Suivi et bibliothèques + L’application n’utilise aucun outil de suivi (mesure d\'audience, rapport d’erreurs, etc.) et elle ne comporte aucune publicité.\n\n + L’utilisation de bibliothèques est réduite au strict minimum :\n + - Glid : Pour la gestion des médias\n + - Android-Job : Pour la gestion des services\n + - PhotoView : Pour la gestion des images\n + + Traduction des pouets + L’application offre la possibilité de traduire les pouets en utilisant les paramètres régionaux de l’appareil et l’API de Yandex.\n + Yandex a sa propre politique de confidentialité qui peut être consultée à l\'adresse suivante : https://yandex.ru/legal/confidential/?lang=en + + Merci à : + + Filtrer avec une expression régulière + Rechercher + Supprimer + Retrouver plus de pouets… + + Listes + Êtes-vous sûr de vouloir supprimer définitivement cette liste ? + Il n’y a rien dans cette liste pour l’instant. + Ajouter à la liste + Ajouter une liste + Supprimer la liste + Modifier la liste + Titre de la nouvelle liste + Le compte a été ajouté à la liste ! + Vous n’avez pas encore de listes ! + + %1$s a été déplacé vers %2$s + Problème de connexion ? + Voici quelques vérifications qui pourraient aider : \n\n +- Vérifier qu’il n’y a pas d’erreur dans le nom de l’instance\n\n +- Vérifiez que votre instance est fonctionnelle\n\n +- Si vous utilisez l’authentification en deux étapes (2FA), veuillez utiliser le lien en bas (une fois le nom de l’instance renseigné) \n\n +- Vous pouvez également utiliser ce lien sans utiliser la 2FA\n\n +- Si cela ne fonctionne toujours pas, vous pouvez reporter ce problème sur FramaGit : https://framagit.org/tom79/fedilab/issues + L’image a été chargée. Cliquez ici pour l’afficher. + Cela peut être assez long. Vous serez notifié·e une fois l’exportation finie. + Exportation en cours, veuillez patienter… + Exporter vos pouets + Pouets exportés pour %1$s + %1$s pouets sur %2$s ont été exportés. + Une erreur est survenue lors de l’exportation des pouets pour %1$s + Quelque chose a mal tourné lors de l’exportation de données ! + Quelque chose a mal tourné lors de l’importation de données ! + + Proxy + Activer le proxy ? + Serveur + Port + Identifiant + Mot de passe + Ajouter les détails du pouet en le partageant + Soutenir l’app sur Liberapay + Il y a une erreur dans l’expression régulière ! + Aucun fil n’a été trouvé sur cette instance ! + Désirez-vous effacer cette instance ? + Traduire en + Suivre l’instance + Vous suivez déjà cette instance ! + Maintenant, vous suivez cette instance ! + Partenariats + Information + Masquer les boosts de %s + Recommander + Afficher les boosts de %s + Ne pas recommander + Ce compte est maintenant recommandé sur votre profil + Ce compte n’est plus recommandé sur votre profil + Les boosts sont maintenant masqués ! + Les boosts sont maintenant masqués ! + Message direct + Filtres + Aucun filtre à afficher. Vous pouvez en créer un en cliquant sur le bouton « + ». + Mot clé ou une expression + Fil principal + Fils publics + Notifications + Discussions + Sera trouvé sans que la casse ou l’avertissement de contenu du pouet soit pris en compte + Supprimer plutôt que de cacher + Les pouets filtrés disparaîtront irrémédiablement, même si le filtre est supprimé ultérieurement + Lorsque le mot-clef ou la phrase-clef est uniquement alphanumérique, ça sera uniquement appliqué s’il correspond au mot entier + Mot entier + Contextes du filtre + Un ou plusieurs contextes où le filtre devrait s’appliquer + Expire après + Supprimer le filtre ? + Mettre à jour le filtre + Créer un filtre + Suggestion de comptes + Actuellement, cette liste ne contient aucun compte ! + Suivre + Tout sélectionner + Tout désélectionner + %s est suivi ! + Création de la liste %s + Rajout des comptes à la liste + Les comptes ont été ajoutés à la liste + Ajout des comptes dans la liste + Vous n\'avez pas encore créé de liste. Cliquez sur le bouton \"+\" pour en ajouter une. + Comptes suggérés + API Trunk + Abonnement impossible au(x) compte(s) + Recherche du compte distant + Étendre automatiquement les médias cachés + Nouvel abonnement + Nouveau partage + Nouveau Favori + Nouvelle mention + Sondage terminé + Nouveau pouet + Sauvegarde des Pouets + Nouveaux messages + Téléchargement des Médias + Changer le son des notifications + Choisir une sonnerie + Activer le créneau horaire + Tutoriels vidéo + Récupération du fil distant ! + Aucun domaine bloqué ! + Débloquer le domaine + Confirmez-vous le déblocage de %s ? + Êtes-vous sûr de vouloir bloquer %s ? + Les domaines bloqués + Bloquer le domaine + Le domaine est bloqué + Le domaine n\'est plus bloqué ! + Récupération du statut distant + Commenter + Instance PeerTube + Soyez le·a premier·ère à laisser un commentaire sur cette vidéo en utilisant le bouton supérieur droit ! + %s vues + Durée : %s + Ajouter une instance + Les commentaires sur cette vidéos ont été désactivés ! + Choisissez une résolution + Favoris PeerTube + La vidéo est rajoutée aux favoris ! + La vidéo a été retirée de vos favoris ! + Il n’y a aucune vidéo Peertube dans vos favoris ! + Chaîne + Vidéos + Chaînes + Utiliser les EmojiOne + information + Afficher les aperçus dans tous les pouets + Nouveau·lle UX/UI Designer·use + Afficher les aperçus des vidéos + L’ID du compte été copié vers le presse-papiers ! + Changer de langue + Langue par défaut + Tronquer les longs pouets + Tronquer les pouets dépassant « x » lignes. Zéro signifie désactivé. + Afficher plus + Afficher moins + Gestion des étiquettes + Cette étiquette existe déjà ! + L’étiquette a été restaurée ! + L’étiquette a été modifiée ! + L’étiquette a été supprimée ! + Programmer un boost + Le boost est programmé ! + Aucun boost programmé à afficher ! + Programmer un boost.]]> + Fil artistique + Ouvrir le menu + Retour + Logo de l’application + Photo du profil + Bannière du profil + Contacter l’administrateur de l’instance + Ajouter un nouveau + Logo de MastoHost + Sélecteur d’émojis + Actualiser + Développer la conversation + Supprimer un compte + Supprimer le domaine bloqué + Sélecteur d’émojis personnalisé + Lire la vidéo + Nouveau pouet + Image de la carte + Masquer les médias + Favicon + Image où la description va être ajoutée + + Jamais + 30 minutes + 1 heure + 6 heures + 12 heures + 1 jour + 1 semaine + + Renseignez le nom de domaine de votre instance dans ce champs.\nPar exemple, si vous avez créé un compte sur https://mastodon.social\nécrivez simplement mastodon.social(sans le https://)\n +Commencez à saisir les premières lettres et des domaines vous seront suggérés. \n\n +Le bouton de connexion s’activera une fois qu’un domaine valide sera renseigné et que l’instance est active ! + Plus d’informations + + Langues + Médias seulement + Images sensibles + Traductions de Crowdin + Manager des traductions Crowdin + Traduire l’application + À propos de Crowdin + Robot + Instance PixelFed + Instance Mastodon + N’importe lequel + Tous + Aucun + N’importe lequel de ces mots (séparés par des espaces) + Tous ces mots (séparés par des espaces) + Ajouter quelques mots à filtrer (séparés par un espace) + Renommer la colonne + Instance Misskey + Aucune application prenant en charge ce lien n’est installée sur votre appareil. + Abonnements + Vue d’ensemble + Tendances + Récemment ajoutées + Locales + Téléverser + Répondre + Supprimer le commentaire + Etes-vous sûr de vouloir supprimer ce commentaire ? + Vidéo plein écran + Mode pour les vidéos + Sélectionnez un fichier à transférer + Mes vidéos + Titre + Licence + Catégorie + Langue + Cette vidéo contient du contenu pour adultes + Activer les commentaires + Mettre à jour la vidéo + Description + La vidéo a été mise à jour ! + Transfert annulé ! + La vidéo a été transférée ! + Transfert en cours, veuillez patienter … + Cliquez ici pour éditer les données de la vidéo. + Supprimer la vidéo + Êtes-vous sûr de vouloir supprimer cette vidéo ? + Afficher les vidéos sensibles + Aucune vidéo n’est disponible ! + Laisser un commentaire + Partager + Choisissez un mode pour la planification + Depuis l’appareil + Depuis le serveur + Pouets (Serveur) + Pouets (Appareil) + Modifier + Afficher les nouveaux pouets au-dessus du bouton « Afficher le contenu ? » + Les fils + Interface + Contacts + %1$s a commenté votre vidéo %2$s]]> + %1$s suit votre chaîne %2$s]]> + %1$s suit votre compte]]> + %1$s a été publiée]]> + %1$s a réussi]]> + %1$s]]> + %1$s a publié une nouvelle vidéo : %2$s]]> + %1$s a été blacklisté]]> + %1$s n’est plus blacklisté]]> + Exporter les données + Importer les données + Sélectionner le fichier à importer + Une erreur s’est produite lors de la sélection du fichier de sauvegarde ! + Ajouter un commentaire public + Envoyer un commentaire + Il n’y a pas de connexion Internet. Votre message a été stocké dans les brouillons. + Texte brut + HTML + Markdown + Déconnexion du compte + Tout + Soutenir l’application + Open Collective permet aux groupes de créer rapidement un collectif, de collecter des fonds et de les gérer de manière transparente. + Copier le lien + Connecter + Normal + Compact + Terminal + Définir le mode d’affichage + Patcher le fournisseur de sécurité + Mettre à jour les domaines de suivi + La base de données de suivi a été mise à jour ! + appels http bloqués par l’application + Liste des appels bloqués + Envoyer + La base de données a été exportée! + Hashtags recommandés + Filtrer la timeline avec des tags + Aucune étiquette + Cacher le bouton de suppression de notification sur l\'onglet de notification + Attacher une image lors du partage d\'une URL + + Sondage + Sondages + Créer un sondage + Choix 1 + Choix 2 + Choix %d + Vous avez besoin d\'au moins deux choix pour le sondage ! + Fait + fin à %s + Actualiser le sondage + Voter + Un sondage auquel vous avez participé est maintenant terminé + Un sondage que vous avez publié est maintenant terminé + Personnaliser + Catégories + Créneau horaire + Avancé + Afficher le badge \'new\' sur les pouets non lus + PeerTube + Déplacer le fil + Cacher le fil + Réorganiser les fils + Liste supprimée définitivement + Instance suivie supprimée + Balise épinglée supprimée + Annuler + Vous devez garder deux onglets visibles ! + Réorganiser les fils + Les fils principaux ne peuvent qu’être masqués ! + BBCode + Toujours marquer le média comme sensible + Instance GNU + Statut en cache + Transférer les tags dans les réponses + Appui long pour stocker les médias + Flouter les médias sensibles + Afficher les fils publics en liste + Afficher les fils publics + Marquer les comptes bot dans les pouets + Gestion des étiquettes + Se rappeler de la position sur le fil principal + Historique + Listes de lecture + Nom d\'affichage + Vous n\'avez aucune liste de lecture. Cliquez sur l\'icône « + » pour en ajouter une + Vous devez fournir un nom d\'affichage ! + Un canal est requis lorsque la liste de lecture est publique. + Créer une liste de lecture + Cette liste de lecture est vide. + rétablir + Galerie + Émoji + Autocollant + Gomme + Texte + Filtre + Pinceau + Êtes-vous sûr de vouloir quitter sans enregistrer l\'image ? + Abandonner + Enregistrement … + Image enregistrée avec succès ! + Échec de l\'enregistrement de l\'image + Transparence + Activer l\'éditeur d\'images + Ajouter un élément de sondage + Supprimer le dernier élément de sondage + Mettre la conversation en sourdine + Enlever la sourdine de la discussion + La conversation n\'est plus mise en sourdine ! + La conversation est mise en sourdine + Ouvrez les fonctionnalités de l\'application + Temporairement muet + Mentionner le compte + Rafraîchir le cache + Mentionner le statut + Actualité + Général + Régional + Art + Journalisme + Activisme + Jeux + Technologie + Contenu pour adultes + Furry + Nourriture + Logo de l’instance + Une erreur s\'est produite lors de la vérification des instances disponibles ! + Rejoindre Mastodon + Choisissez une instance dans l\'une des catégories puis cliquez sur un bouton de vérification. + Choisissez une instance en appuyant sur le bouton cocher. + %1$s utilisateur·rice·s + Confirmer le mot de passe + J\'accepte les %1$s et les %2$s + règles du serveur + conditions de service + S’inscrire + Cette instance fonctionne avec des invitations. Votre compte devra être approuvé manuellement par un·e administrateur·rice pour qu\'il devienne utilisable. + Veuillez remplir tous les champs ! + Les mots de passe ne sont pas identiques ! + L\'e-mail ne semble pas être valide ! + Votre nom d\'utilisateur·rice sera unique sur %1$s + Vous recevrez un e-mail de confirmation + Utilisez au moins 8 caractères + Le mot de passe doit contenir au moins 8 caractères + Le nom d\'utilisateur·rice doit contenir uniquement des lettres, des chiffres et des caractères de soulignement + Compte créé ! + Your account has been created!\n\n + Think to validate your email within the 48 next hours.\n\n + You can now connect your account by writing %1$s in the first field and click on Connect.\n\n + Important: If your instance required validation, you will receive an email once it is validated! + + Enregistrer le message dans dans les brouillons ? + Administration + Rapports + Aucun rapport à afficher ! + Reconnecter le compte + L\'application n\'a pas accès aux fonctionnalités d\'administration. Vous devrez peut-être reconnecter le compte pour obtenir les autorisations nécessaires. + Non résolu + Distant + Actif + En attente + Désactivé + Réduit au silence + Suspendu + Permissions + Statut de l\'e-mail + État de connexion + Inscrit le + IP la plus récente + Avertir + Désactiver + Mettre en sourdine + Notifier l\'utilisateur·rice par e-mail + Avertissement personnalisé + Utilsateur·rice + Modérateur·rice + Administrateur·rice + Confirmée + Non confirmée + Publications signalées + Compte + Annuler la sourdine + Annuler la désactivation + Suspendre + Annuler la suspension + Le compte est mis en sourdine ! + Le compte n\'est plus mis en sourdine ! + Le compte est suspendu ! + Le compte n\'est plus suspendu ! + Le compte est désactivé ! + Le compte n\'est plus désactivé ! + Le compte a été averti ! + Afficher le menu administrateur + Afficher la fonctionnalité d\'admin dans les pouets + Autoriser + Le compte est approuvé ! + Le compte est rejeté ! + Me l’attribuer + Annuler l\'assignation + Marquer comme résolu + Marquer comme non résolu + Contenu vide ! + Afficher le bouton des fonctionnalités de Fedilab + L\'application a besoin d\'accéder à l\'enregistrement audio + Message vocal + Activer la réponse rapide + Le compte auquel vous répondez ne peut voir votre message ! + Si désactivé, l\'application chargera toujours les derniers statuts + Si désactivé, les médias sensibles seront cachés avec un bouton + Enregistre les médias en plein format lors d\'un appui long sur les prévisualisations + Ajoute un bouton dans le coin supérieur droit pour lister tous les tags/instances/listes + Durant cette période de temps, l’application enverra des notifications. Vous pouvez inverser (i. e. : silencer) ce laps de temps à l’aide du sélecteur. + Affiche un bouton Fedilab. C\'est un raccourci pour accéder aux fonctionnalités de l\'application. + Permet de répondre directement dans les fils, en bas des statuts + Les aperçus ne seront pas rognés dans les fils + Autoriser à lire les vidéos intégrées directement dans les fils + Permet d’inverser la façon de lire les publications affichées après avoir cliqué sur le bouton pour retrouver plus de pouets + Cette option permet de prendre en charge les suites de chiffrement récentes. Elle est utile pour les anciens appareils Android ou si vous ne n\'arrivez pas vous connecter à votre instance. + Exclusivement pour les vidéos Peertube. Basculez vers ce mode si vous ne pouvez pas les lire. + Ces tags permettent de filtrer les statuts dans les profils. Vous devrez utiliser le menu contextuel pour les voir. + Insère automatiquement une nouvelle ligne après la mention afin de mettre la première lettre en majuscule + Permet aux créateurs de contenu de partager leurs statuts sur leurs flux RSS + Rédaction + Nombre maximum de tentatives lors du téléversement de médias + Créer un nouveau dossier ici + Entrez le nom du dossier + Veuillez saisir un nom de dossier valide + Ce dossier existe déjà.\n Veuillez fournir un autre nom pour le dossier + Sélectionner + Dossier par défaut + Dossier + Créer un dossier + Afficher un message après qu’une action soit terminée (partage, favori, etc.) ? + Les instances mises en sourdine ont été exportées ! + Ajouter une instance + Exporter des instances + Importer des instances + Rapports de plantage + Activer les rapports de plantage + Si activé, un rapport de plantage sera créé localement et vous pourrez le partager. + Fedilab s’est arrêté :( + Vous pouvez m’envoyer le rapport d’erreur par mail. Cela aidera à corriger le problème :)\n\nVous pouvez y ajouter des informations supplémentaires. Merci ! + Utiliser l’éditeur visuel + Lorsque cette option est activée, vous pouvez mettre en forme votre texte facilement via des outils. + Statistiques + Total des publications + Nombre de partages + Nombre de favoris + Nombre de mentions + Nombre d\'abonnements + Nombre de sondages + Nombre de réponses + Nombre de publications + Publications + Visibilité + Contenant des médias + Contenant des médias sensibles + Ayant avertissement de contenu + Date de première publication + Date de dernière publication + Date de la première notification + Date de la dernière notification + Fréquence + %s publications par jour + %s notifications par jour + Période + Groupes + Aucun groupe ! + Désactiver les émojis animés personnalisés + Graphiques + Afficher les graphiques + L’application collecte vos données locales, veuillez patienter … + Sauvegarder + Sauvegarde automatique des publications + Cette option est spécifique à chaque compte. Elle lancera un service qui va automatiquement stocker vos publications localement dans une base de données. Cela permet d’avoir des statistiques et des graphiques + Sauvegarde automatique des notifications + Cette option est spécifique à chaque compte. Elle lancera un service qui enregistrera automatiquement vos notifications dans une base de données locale. Cela permet d’avoir des statistiques et des graphiques + Signaler le compte + Envoyer une invitation + Votre instance ne permet pas de créer de nouveaux comptes ! + + %d voix + %d voix + + + %d votant + %d votants + + + Choix unique + Choix multiple + + + 5 minutes + 30 minutes + 1 heure + 6 heures + 1 jour + 3 jours + 7 jours + + + Torrent + Flux direct + + Pour rejoindre mon instance « %1$s », vous pouvez télécharger Fedilab :\n\nF-Droid : %2$s\nGoogle : %3$s\n\nEnsuite ouvrez le lien ci-dessous avec Fedilab et créez votre compte :)\n\n%4$s + + Votre sondage ne peut contenir des options dupliquées ! + Pour tous les comptes + Cache de la base de données + Vider le cache de votre fil principal + Effacer vos publications mises en cache + Effacer vos signets + Fichiers en cache + Notifications totales + Cacher les éléments du menu + Fedilab est à l’écoute des notifications en direct + Pour %1$s comptes avec %2$s événements + Notifications en direct pour %1$s + Les notifications en direct seront activées uniquement pour ce compte. + Vider le cache en quittant + Le cache (média, messages en caches, données du navigateur intégré) sera automatiquement vidé lorsque vous quittez l’application. + Voulez-vous vous désabonner de ce compte ? + Afficher une boîte de dialogue de confirmation avant tout désabonnement + Remplacer YouTube par Invidio.us + Indivious est une alternative visuelle pour YouTube + Entrez votre nom de domaine personnalisé ou laissez vide pour utiliser invidio.us + Remplacer Twitter par Nitter + Nitter est une alternative visuelle open source pour Twitter axée sur la vie privée. + Entrez votre nom de domaine personnalisé ou laissez vide pour utiliser nitter.net + Remplacer Instagram par Bibliogram + Bibliogram est une interface ouverte en alternative à Instagram axée sur la confidentialité. + Entrez votre hôte personnalisé ou laissez vide pour utiliser bibliogram.art + Remplacer Reddit par Libreddit + Libreddit est une alternative ouverte à l\'interface de Reddit axée sur la protection de la vie privée. + Entrez votre hôte personnalisé ou laissez vide pour utiliser libredd.it + Remplacer les liens Medium + Remplacer les liens medium.com par une interface open source axée sur la confidentialité. + Par défaut: scribe.rip + Remplacer les liens Wikipédia + Remplacer les liens Wikipédia par une interface open source axée sur la confidentialité. + Par défaut: wikiless.org + Masquer la barre de notification de Fedilab + Pour cacher le reste de la notification dans la barre de statut, cliquez sur le bouton en forme d\'œil, puis décochez: \"Afficher dans la barre de statut\" + Utilise un système de notifications poussées pour obtenir des notifications en temps réel. + Aucune notification en direct + Notifications en direct + Les notifications seront récupérées toutes les 15 minutes. + Ajouter des notes + Notes pour le compte + Permet de comprimer de grandes photos en photos plus petites avec une perte de qualité d\'image très petite ou négligeable. + Permet de compresser les vidéos tout en préservant leur qualité. + L\'application est en train de compresser le média, cela peut être assez long… + Changer l’icône de l’application + Appuyez pour changer l’icône de l’application + Poster + Visibilité du post + Appuyez ici pour ajouter des photos + Formats acceptés: jpeg, png, gif \n\nTaille maximale du fichier : 15 Mo \n\nLes albums peuvent contenir jusqu’à 4 photos ou vidéos + Téléverser un média + Ajouter une légende optionnelle + L’application a reçu un très long message d’erreur de l’API %1$s + Aperçu du message + Ajouter les mentions dans chaque message + Récupération de la discussion + Trier par + Titre de la vidéo + Rejoignez Peertube + J\'ai au moins 16 ans et je suis d\'accord avec les %1$s de cette instance + Liens + Changer la couleur des liens (URL, mentions, tags, etc.) dans les messages + En-tête des partages + Changer la couleur du nom affiché en haut des messages + Changer la couleur du nom de l\'utilisateur en haut des messages + Modifie la couleur de l’en-tête des partages + Posts + Couleur du fond des post des fils + Réinitialiser les couleurs + Cliquez ici pour réinitialiser toutes vos couleurs personnalisées + Réinitialiser + Icônes + Couleur des icônes du bas dans les fils + Épingler ce tag + Logo de l\'instance + Éditer le profil + Faire une action + Traduction + Aperçu de l\'image + Couleur du texte + Changer la couleur du texte dans les publications/messages + Appliquer les modifications + Vous devez redémarrer l\'application pour appliquer les modifications + Redémarrer + Utiliser un thème personnalisé + Permettre de modifier les couleurs du thème sélectionné ci-dessus + Thème + Sauvegarder au préalable + Le thème a été exporté + Le thème a été exporté avec succès en CSV + Appliquer la couleur primaire à la barre d\'état + Couleur de la barre d\'état + Restaurer un thème par défaut + Importer un thème + Appuyez ici pour importer un thème depuis un export précédent + Exporter le thème + Appuyez ici pour exporter le thème actuel + Une erreur s\'est produite lors de la sélection du fichier de thème + Sélecteur de thème + Sélectionner un thème préinstallé + Thèmes + Appliquer la couleur principale à la barre de navigation + Couleur de la barre de navigation + Couleur sous-jacente du contenu de l’application. + Couleur du fond + Les Accents sélectionnent des parties de l\'interface utilisateur. + Couleur d\'accentuation + Affiché le plus souvent dans l\'application. + Couleur primaire + Exporter les signets vers l\'instance + Importer les signets de l\'instance + Nombre d\'utilisateurs + Nombre de statuts + L’instance comprend + Bloqué + Fin dans %s + Quoi de neuf dans %s + Vous pouvez suivre mon compte pour les mises à jour + Cette instance n’est pas disponible sur https://instances.social + Afficher le lien complet + Partager le lien + L\'url a été copiée dans le presse-papiers + Ouvrir avec une autre application + Vérifier la redirection + Cette URL ne redirige pas + %1$s\n\nredirige vers\n\n%2$s + Modifier l\'agent d\'utilisateur + Définir un agent utilisateur personnalisé ou laisser vide + Permet de personnaliser l\'agent de l\'utilisateur utilisé pour les appels de l\'api ou avec le navigateur intégré. + Supprimer les paramètres UTM + L’application supprimera automatiquement les paramètres UTM des URL avant de visiter un lien. + Tendances + Tendance en ce moment + %d personnes en parlent + Comptes Twitter (via Nitter) + Noms d\'utilisateurs Twitter séparés par un espace + Preuves d’identité + Identité vérifiée + Vérifié par %1$s (%2$s) + Supprimer la notification + Afficher plus d\'options + C\'est une histoire Pixelfed + Téléversez un média, il sera automatiquement ajouté à votre histoire Pixelfed. + Média ajouté avec succès à votre histoire! + Action désactivée + Se désabonner + Une erreur s’est produite, veuillez vérifier votre répertoire de téléchargement dans les paramètres. + Annonces + Aucune annonce ! + Ajouter une réaction + Utiliser votre navigateur favori dans l’application. Désélectionner cette fonction pour ouvrir les liens en externe. + Cache vidéo en Mo, zéro signifie pas de cache. + Filigranes + Ajouter automatiquement un filigrane au bas des images. Le texte peut être personnalisé pour chaque compte. + Aucun distributeur trouvé ! + Vous avez besoin d’un distributeur pour recevoir des notifications poussées.\nVous trouverez plus de détails sur %1$s.\n\nVous pouvez également désactiver les notifications poussées dans les paramètres pour ignorer ce message. + Sélectionnez un distributeur + diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml new file mode 100644 index 000000000..d2742dd74 --- /dev/null +++ b/app/src/main/res/values-gl/strings.xml @@ -0,0 +1,1142 @@ + + + Abrir o menú + Pechar o menú + Acerca de + Acerca da instancia + Intimidade + Caché + Desconectar + Conectar + + Pechar + Si + Non + Cancelar + Descargar + Descargar %1$s + Medios gardados + Ficheiro: %1$s + Contrasinal + Correo-e + Contas + Toots + Etiquetas + Gardar + Restaurar + Sen resultados! + Instancia + Instancia: mastodon.social + A funcionar coa conta %1$s + Engadir unha conta + Copiouse ao portapapeis o contido do toot + Copiouse o URL do toot ao portapapéis + Cambiar + Escoller imaxe… + Limpar + Cámara + Elimnar todo + Traducir este toot. + Programar + Tamaño do texto e iconas + Cambiar o tamaño actual do texto: + Cambiar o tamaño actual das iconas: + Seguinte + Anterior + Abrir con + Validar + Medios + Compartir con + Compartido con Fedilab + Respostas + Nome de usuaria + Borradores + Favoritas + Novas seguidoras + Mencións + Promocións + Mostrar promocións + Mostrar respostas + Abrir en navegador + Traducir + Por favor, agarde uns segundos antes de realizar esta acción. + + Inicio + Liña temporal local + Liña temporal federada + Opcións + Favoritas + Comunicación + Usuarias acaladas + Usuarias bloqueadas + Notificacións + Solicitudes de seguimento + Axustes + Eliminar a conta + Eliminar a conta %1$s de esta aplicación? + Enviar un correo-e + Pulse na ruta para cambiala + Fallo! + Toots programados + A información inferior sobre a usuaria podería estar incompleta. + Incrustar emoji + Polo momento a app non permite emojis personalizados. + Notificacións Push + Tes a certeza de querer saír? + Tes a certeza de querer desconectar @%1$s@%2$s? + + Sen toot que mostrar + Non hai historias que mostrar + Historias + Promovido por %1$s + Engadir este toot aos seus favoritos? + Eliminar este toot dos seus favoritos? + Promover este toot? + Deixar de promover o toot? + Fixar este toot? + Liberar este toot? + Acalar + Bloquear + Informar + Eliminar + Copiar + Compartir + Citar + Acalar temporalmente + Eliminar & Editar + + Acalar esta conta? + Bloquear esta conta? + Informar sobre este toot? + Bloquear este dominio? + Voltar a ler esta conta? + Desbloquear esta conta? + + + Notificar + Silencioso + + + Eliminar este toot? + Eliminar & rescribir este toot? + + Marcadores + Engadir a marcadores + Eliminar marcador + Sen marcadores que mostrar + O estado foi engadido a marcadores! + Eliminouse o estado dos marcadores! + + %d s + %d m + %d h + %d d + + %d segundo + %d segundos + + + %d minuto + %d minutos + + + %d hora + %d horas + + + %d día + %d días + + + Aviso + Que contas? + TOOT! + QUEET! + cw + Escriba un toot + Resposte a un toot + Escriba un queet + Resposte a un queet + Escolla medios + Algo fallou ao engadir os medios! + Eliminar este elemento? + O seu toot está baldeiro! + Visibilidade do toot + Visibilidade por omisión para os toots: + O toot foi enviado! + Está a respostar a este toot: + Contido sensible? + + Publicar en liñas temporais públicas + Non publicar en TL públicas + Só para seguidoras + Só para as usuarias mencionadas + + Sen borradores! + Escolla un toot + Escolla unha conta + Escolla varias contas + Eliminar borrador? + Pulse no botón para mostrar o toot orixinal + Descrición para usuarias con deficiencias visuais + + Sen descrición dispoñible! + + Versión %1$s + Desenvolvedora: + Licenza: + GNU GPL V3 + Código fonte: + Tradución dos toots: + Buscar instancias: + Deseño de iconas: + + Conversa + + Sen conta que mostrar + Sen petición de seguimento + Toots \n %1$s + Seguindo \n %1$s + Seguidoras \n %1$s + Fixadas \n %d + Autorizar + Rexeitar + + Sen toots programados! + Escriba un toot e escolla Programar no menú superior. + Borrar o toot programado? + Medios: %d + O toot foi programado! + A data programada debe ser posterior a hora actual! + Está activado o aforro de enerxía! Podería non funcionar como debe. + + O tempo de silencio debe ser maior de un minuto. + %1$s foi acalado ate %2$s.\n Pode voltar a activar a conta desde a súa páxina de perfil. + %1$s foi silenciado ate %2$s.\n Pulse aquí para voltar activalo. + + Sen notificación que mostrar + mencionouna + escribeu unha nova mensaxe + promoveu o seu estado + favoreceu o seu estado + segueuna + solicitou seguirte + + e outra notificación + e outras %d notificacións + + + %d gústame + %d gústame + + Borrar notificación? + Borrar todas as notificacións? + Eliminouse a notificación! + Borráronse todas as notificacións! + + Seguindo + Seguidoras + Fixados + + Imposible obter o id de cliente! + Non se puido conectar co dominio da instancia! + Sen conexión a internet! + A conta foi bloqueada! + A conta xa non está bloqueada! + A conta foi acalada! + A conta xa non está acalada! + A conta foi seguida! + A conta xa non está seguida! + O toot foi promovido! + O toot xa non está promovido! + Este toot foi engadido aos seus favoritos! + Este toot eliminouse dos favoritos! + Informou sobre este toot! + Eliminou o toot! + O toot foi fixado! + O toot foi liberado! + Vaia! Algo fallou! + Algo fallou! A instancia non devolveu o código de autorización! + O dominio da instancia non semella ser válido! + Algo fallou mentras cambiaba de conta! + Algo fallou ao buscar! + Gardáronse os datos do perfil! + Non se poden realizar accións + Gardáronse os medios! + Algo fallou ao traducir! + As traducións están desactivadas nos axustes + Borrador gardado! + Está segura de que esta instancia permite este número de caracteres? Habitualmente este valor está próximo aos 500 caracteres. + Cambiouse a visibilidade dos toots para a conta %1$s + + Número de toots por carga + Sempre + WiFi + Solicitar + Cargar medios + Cargar imaxes + Mostrar máis… + Mostrar menos… + Contido sensible + Desactivar avatares GIF + Ruta: + Gardar borradores automáticamente + Engadir a URL dos medios nos toots + Notificar cando alguén a segue + Notificar cando alguén promove un dos seus toots + Notificar cando alguén favorece un dos seus estados + Notificar cando alguén a menciona + Notificar cando remate unha sondaxe + Notificar as novas publicacións + Solicitar confirmación antes de promover + Solicitar confirmación antes de engadir a favoritos + Notificar só cando WiFi + Notificar? + Notificacións silenciosas + Caducidade da vista NSFW (en segundos, 0 é apagado) + Caducidade Descrición Medios (segundos, 0 igual a off) + Editar perfil + Compartición personalizada + O URL da súa compartición… + Bio… + Bloquear conta + Gardar cambios + Escoller unha imaxe de cabeceira + Axustar imaxe vista previa + Dividir automáticamente os toots de máis de 500 nas respostas + Acadou o límite de 160 caracteres permitido! + Acadou os 30 caracteres permitidos! + Entre + e + A tempo debe ser maior de %1$s + O tempo debe ser menor de %1$s + Hora de inicio + Hora de fin + Utilizar o navegador incluído + Pestana personalizadas + Activar javascript + Expandir automáticamente CW + Permitir testemuños de terceiros + A súa chave API, pode deixala en branco para Yandex + + Escuro + Claro + Negro + + Cor do LED: + + Azul + Ciano + Maxenta + Verde + Vermello + Amarelo + Branco + + Seguir + Desbloquear + Acalar + Non acalar + Petición enviada + Séguea + Buscar + Primeira letra en maiúscula para as respostas + Escalar imaxes + Escalar vídeos + + Notificacións Push + Por favor, confirme cales son as notificacións push que quere recibir. + Posteriormente pode activar ou desactivar estas notificacións en Axustes (pestana Notificacións). + + + Limpar caché + Hai %1$s de datos na caché.\n\nDesexa eliminalos? + Mb + Limpouse a caché! Liberáronse %1$s + + Título + Título… + Descrición + Palabras chave + Palabras chave… + + Sincronizar + Filtrar + Os seus toots + Notificacións + Público + Non listado + Privado + Directo + Palabras chave… + Mostrar medios + Mostrar fixados + Non se atoparon resultados! + Respaldar toots para %1$s + %1$s novos toots foron importados + Importáronse %1$s novas notificacións + + Data descendente + Data ascendente + + + Non + + Ambos + + Non se atoparon toots na base de datos. Por favor, utilice o botón de sincronización no menú para obtelos. + + Datos gravados + Só se garda no dispositivo a información básica das contas. + Estos datos son totalmente confidenciais e só poden ser utilizados pola aplicación. + Eliminando a aplicación eliminará inmediatamente estos datos.\n + ⚠ Usuario e Contrasinal nunca se gardan. Só se utilizan no proceso de autenticación (SSL) coa instancia. + + Permisos: + - ACCESS_NETWORK_STATE: Utilizado para detectar si o dispositivo está conectado a unha rede WiFi.\n + - INTERNET: Utilizado para as consultas a instancia.\n + - WRITE_EXTERNAL_STORAGE: Utilizado para gardar medios ou mover a app a tarxeta SD.\n + - READ_EXTERNAL_STORAGE: Utilizado para engadir medios aos toots.\n + - BOOT_COMPLETED: Utilizado para iniciar o servizo de notificacións.\n + - WAKE_LOCK: Utilizado durante o servizo de notificacións. + + Permisos API: + - Lectura: Ler datos.\n + - Escritura: Publicar estados e subir medios para os estados.\n + - Seguimento: Seguir, deixar de seguir, bloquear, desbloquear.\n\n + ⚠ Estas accións só se realizan a petición da usuaria. + + Rastrexo e Bibliotecas + A aplicación non utiliza ferramentas de rastrexo (medida de audiencias, reporte de fallos, etc.) e non contén publicidade.\n\n + O uso de bibliotecas foi minimizado: \n + - Glide: Para xestionar medios\n + - Android-Job: Para xestionar servizos\n + - PhotoView: Para xestionar imaxes\n + + Tradución dos toots + A aplicación ofrece a posibilidade de traducir os toots utilizando o idioma do dispositivo e a API de Yandex.\n + Yandex ten a súa propia política de intimidade, que pode atopar aquí: https://yandex.ru/legal/confidential/?lang=en + + Grazas a: + + Filtrado de expresións regulares + Buscar + Eliminar + Obter máis toots… + + Listaxes + Está segura de querer eliminar esta listaxe? + Aínda non hai nada na lista. Cando as usuarias da lista publiquen novos estados, aparecerán aquí. + Engadir a lista + Engadir lista + Eliminar lista + Editar lista + Novo título da listaxe + A conta foi engadida a lista! + Aínda non tes ningunha lista! + + %1$s mudouse a %2$s + Non funciona a autenticación? + Aquí ten unhas comprobacións que poderían axudarlle:\n\n + - Comprobe que non ten fallos escribindo o nome da instancia\n\n + - Comprobe que a súa instancia está a funcionar\n\n + - Si utiliza autenticación de doble factor (2FA), utilice a ligazón inferior (unha vez escriba o nome da instancia)\n\n + - Pode utilizar tamén esta ligazón sin ter que usar a 2FA\n\n + - Si aínda non pode conectar, por favor suba o problema a Framagit en https://framagit.org/tom79/fedilab/issues + + Cargáronse os medios. Pulse aquí para mostralos. + Esta acción podería prolongarse. Notificarémoslle cando esté rematada. + Aínda a traballar, agarde por favor… + Exportar estados + Exportar estados para %1$s + %1$s toots de %2$s foron exportados. + Algo fallou mentras se exportaban os datos para %1$s + Algo fallou ao exportar os datos! + Algo fallou ao importar os datos! + + Proxy + Activar proxy? + Host + Porto + Conectar + Contrasinal + Engadir detalles do toot ao compartir + Axude a app en Liberapay + Hai un fallo na expresión regular! + Non se atoparon liñas temporais en esta instancia! + Eliminar esta instancia? + Traducir en + Seguir instancia + Xa segue a esta instancia! + Esta a seguir a instancia! + Asociados + Información + Ocultar promocións desde %s + Mostar no perfil + Mostrar promocións desde %s + Non mostrar no perfil + A conta está agora mostrada no perfil + A conta xa non aparece no perfil + Agora móstranse as promocións! + As promocións están ocultas! + Mensaxe directa + Filtros + Sen filtros que mostrar. Pode crear un pulsando no botón \"+\". + Palabra chave ou frase + Liña temporal de inicio + Liñas de tempo públicas + Notificacións + Conversas + Farase coincidir sen importar se é texto agochado ou con aviso de contido na mensaxe + Desbotar en lugar de ocultar + Os toots filtrados desaparecerán irreversiblemente, incluso si despois elimina o filtro + Cando a palabra chave é só alfanumérica, só se aplicará si coincide a palabra completa + Palabra completa + Filtrar contextos + Un o varios contextos onde se debe aplicar o filtro + Caducar tras + Eliminar filtro? + Actualizar filtro + Crear filtro + A quen seguir + Non hai contas listadas por agora! + Seguir + Escoller todo + Desmarcar todo + Está a seguir a %s! + Creando a listaxe %s + Engadindo contas a lista + Engadíronse contas a listaxe + Engadindo contas a listaxe + Aínda non creou listaxes. Pulse no botón \"+\" para engadir unha nova. + A quen seguir + Trunk API + Conta(s) que non poden ser seguidas + Obtendo conta remota + Mostrar automáticamente medios ocultos + Novo seguimento + Nova promoción + Novo favorito + Nova mención + Rematou a sondaxe + Novo Toot + Respaldo de Toots + Novas publicacións + Descarga de medios + Cambiar o tono de notificación + Escoller Tono + Activar tramo horario + HowTo Vídeos + Obtendo fío remoto! + Sen dominios bloqueados! + Desbloquear dominio + Seguro desexa desbloquear %s? + Desexa bloquear a %s? + Dominios bloqueados + Bloquear dominio + O dominio foi bloqueado + O dominio xa non está bloqueado! + Obtendo estado remoto + Comentar + Instancia Peertube + Sexa a primeira en deixar un comentario a este vídeo co botón superior dereito! + %s vistas + Duración: %s + Engadir unha instancia + Non se activaron os comentarios para este vídeo! + Escolle unha resolución + Favoritos de Peertube + Este vídeo foi engadido aos marcadores! + Este vídeo foi eliminado dos marcadores! + Non ten vídeos de Peertube nos seus favoritos! + Canle + Vídeos + Canles + Utilizar Emoji One + Información + Mostrar vista previa en todos os toots + Deseñador/a da nova UX/UI + Mostrar vista previa de vídeos + Copiouse o id de conta ao portapapeis! + Cambiar o idioma + Idioma por omisión + Cortar en varios os toots longos + Repartir os toots superiores a \'x\' liñas. Cero significa desactivado. + Mostrar máis + Mostrar menos + Xestionar etiquetas + A etiqueta xa existe! + Gardouse a etiqueta! + A etiqueta foi cambiada! + Eliminouse a etiqueta! + Programar promoción + A promoción foi programada! + Non ten promocións programadas que mostrar! + Programar promoción.]]> + Liña temporal Art + Abrir menú + Atrás + Logo da aplicación + Imaxe de perfil + Banda do perfil + Contacte coa administración da instancia + Engadir novo + Logo MastoHost + Selector Emoji + Actualizar + Despregar a conversa + Eliminar conta + Eliminar o dominio bloqueado + Selector emoji personalizado + Reproducir vídeo + Novo toot + Imaxe da tarxeta + Ocultar medios + Favicon + Medios aos que engadir descrición + + Nunca + 30 minutos + 1 hora + 6 horas + 12 horas + 1 día + 1 semana + + En este campo, debe escribir o nome do servidor da súa instancia.\nPor exemplo, se creou a súa conta en https://mastodon.social\ndebe escribir mastodon.social (sen https://)\n + Pode iniciar a escribir o nome e irá obtendo suxestións de servidores.\n\n + ⚠ O botón de conexión só estará activo se o nome da instancia é válido e a instancia está activa! + + Máis información + + Idiomas + Só medios + Mostrar NSFW + Traducións en Crowdin + Xestor Crowdin + Tradución da aplicación + Sobre Crowdin + Bot + Instancia Pixelfed + Instancia Mastodon + Calquera de estos + Todos estos + Ningún de estos + Calquera de estas palabras (separadas por espazos) + Todas estas palabras (separadas por espazos) + Engade algunha palabra para filtrar (separadas por espazos) + Cambiar o nome da columna + Instancia Misskey + Non ten ningunha app no dispositivo para este tipo de ligazón. + Suscricións + Vista xeral + Tendencia + Recén engadido + Local + Subir + Resposta + Eliminar comentario + Seguro que quere eliminar o comentario? + Vídeo a pantalla completa + Modo para vídeos + Escolla o ficheiro a subir + Os meus vídeos + Título + Licenza + Categoría + Idioma + Este vídeo contén contido adulto ou explícito + Activar comentarios do vídeo + Actualizar vídeo + Descrición + O vídeo foi actualizado! + Cancelouse a subida! + O vídeo foi subido! + Subindo, agarde por favor… + Pulse aquí para editar datos do vídeo. + Eliminar vídeo + Seguro que quere eliminar este vídeo? + Mostar vídeos NSFW + Sen vídeos que mostrar! + Deixar comentario + Compartir + Escolla un método de programación + Desde dispositivo + Desde servidor + Toots (Servidor) + Toots (Dispositivo) + Modificar + Mostrar novos toots enriba do botón \"Obter máis\" + Liñas temporais + Interface + Contactos + %1$s comentou o seu vídeo %2$s]]> + %1$s está a seguir o seu canal %2$s]]> + %1$s está a seguir a súa conta]]> + %1$s]]> + %1$s]]> + %1$s]]> + %1$s publicou un novo vídeo: %2$s]]> + %1$s foi posto nunha lista negra]]> + %1$s sacouse da lista negra]]> + Exportar datos + Importar datos + Escoller o ficheiro a importar + Algo fallou ao seleccionar o ficheiro de respaldo! + Engadir un comentario público + Enviar comentario + Non hai conexión a internet. A súa mensaxe gardouse nos borradores. + Texto plano + HTML + Markdown + Desconectar conta + Todo + Axude a app + Open Collective crea grupos para establecer rápidamente un colectivo, colleitar fondos e xestionalos de xeito transparente. + Copiar ligazón + Conectar + Normal + Compacta + Consola + Establecer modo de visualización + Provedor de Seguridade + Actualizar dominios de rastrexo + Actualizouse a base de datos de rastrexo! + peticións http bloqueadas pola aplicación + Lista de peticións bloquedas + Enviar + Exportouse a base de datos! + Etiquetas destacadas + Filtrar liña temporal con etiquetas + Sen etiquetas + Ocultar o botón de eliminar notificación na pestana de notificación + Obter metadatos se o URL os comparte desde outras apps + + Sondaxe + Sondaxes + Crear sondaxe + Opción 1 + Opción 2 + Elección %d + Debe establecer dúas opcións como mínimo! + Feito + finaliza en %s + Actualizar sondaxe + Votar + Rematou a sondaxe na que participou + Rematou unha sondaxe na que tooteou + Personalizar + Categorías + Marxe temporal + Avanzado + Mostrar enseña \'novo\' nos toots non lidos + Peertube + Mover liña temporal + Agochar liña temporal + Ordear liñas temporais + Lista eliminada de xeito permanente + Eliminouse o seguimento da instancia + Eliminouse a etiqueta fixada + Desfacer + Debe manter dúas lapelas visibles! + Ordear liñas temporais + As liñas temporais principais só poden ocultarse! + BBCode + Marcar os medios sempre como sensibles + Instancia GNU + Estado almacenado + Incluír etiquetas nas respostas + Manter pulsado para gardar medios + Difuminar medios sensibles + Mostrar liñas temporais nunha lista + Mostrar liñas temporais + Marcar contas de bots nos toots + Xestionar etiquetas + Lembrar posición da liña temporal de Inicio + Historial + Listas de reproducción + Nome público + Non ten listas de reprodución. Pulse na icona \"+\" para engadir unha nova + Debe proporcionar un nome público! + A canle é requerida cando a lista é pública. + Crear lista reproducción + Aínda non hai nada en esta lista. + refacer + Galería + Emoji + Pegatina + Eliminador + Texto + Filtro + Esborranchar + Quere saír sen gardar a imaxe? + Descartar + Gardando… + Imaxe gardada! + Fallo ao gardar a imaxe + Opacidade + Activar editor de fotos + Engadir un elemento a sondaxe + Eliminar un elemento a sondaxe + Acalar conversa + Desbloquear conversa + Esta conversa deixou de estar acalada! + Esta conversa está acalada + Abrir características da aplicación + Acalado temporal + Mencionar a conta + Actualizar caché + Mencionar o estado + Novas + Xeral + Rexional + Arte + Xornalismo + Activismo + Xogos + Tecnoloxía + Contido adulto + Peluxos + Comida + Logo da instancia + Algo fallou cando comprobamos as instancias dispoñibles! + Únirse a Mastodon + Escolla unha instancia escollendo unha categoría, despois pulse no botón de marca. + Escolle unha instancia tocando na marca de selección. + %1$s usuarias + Confirmar contrasinal + Acepto as %1$s e os %2$s + regras do servidor + termos de servizo + Rexistrar + Esta instancia funciona con convites. A súa conta precisa ser aceptada manualmente pola administración antes de poder utilizala. + Por favor, complete todos os campos! + Non coinciden os contrasinais! + O correo-e non semella ser válido! + O seu nome de usuaria será único en %1$s + Enviarémoslle un correo-e de confirmación + Utilice 8 caracteres ao menos + O contrasinal debe conter ao menos 8 caracteres + O nome de usuaria debería ter só letras, números e guión baixo + Conta creada! + Xa ten unha conta!\n\n + Lembre validar o correo-e nas seguintes48 horas.\n\n + Xa pode conectar coa súa conta escribindo %1$s no primeiro campo e pulsando Conectar.\n\n + Importante: se a súa instancia require validación, recibirá un correo unha vez sexa validada! + + ¿Gardar mensaxe en borradores? + Administración + Informes + Non hai informes! + Reconectar a conta + A aplicación non puido acceder ao área de administración. Podería ter que voltar a conectar para ter o nivel de acceso correcto. + Sen resolver + Remoto + Activo + Pendente + Desactivado + Acalado + Suspendido + Permisos + Estado do correo-e + Estado de conexión + Unido + IP máis recente + Avisar + Desactivar + Acalar + Enviar aviso por correo-e + Aviso personalizado + Usuaria + Moderación + Administración + Confirmado + Non confirmado + Estados reportados + Conta + Desfacer acalar + Desfacer desactivar + Suspender + Desfacer suspender + A conta está acalada! + A conta xa non está acalada! + A conta está suspendida! + A conta xa non está suspendida! + A conta está desactivada! + A conta xa non está desactivada! + A conta foi advertida! + Mostrar menú de administración + Mostrar ferramentas de admin nos estados + Permitir + A conta foi aprobada! + A conta foi rexeitada! + Asignarme a tarefa + Non asignar + Marcar como resolto + Marcar como non resolto + Sen contido! + Mostrar botón características Fedilab + A aplicación precisa acceso a gravación de audio + Mensaxe de voz + Activar resposta rápida + A conta a que está a respostar podería non ver a súa mensaxe! + Se desactivado, a app sempre cargará os últimos estados + Se desactivado, os medios sensibles estarán ocultos con un botón + Gardar medios a resolución completa con pulsación longa na vista previa + Engadir un botón elíptico arriba a dereita para listar todas as etiquetas/instancias/listas + Durante o tempo marcado, a app enviará notificacións. Pode revertir (silenciar) esta marxe coa roda da dereita. + Mostar un botón Fedilab debaixo da imaxe de perfil. É un atallo para acceder a funcións propias da app. + Permitir respostar directamente en liñas temporais baixo os estados + Non se recortarán as vistas previas en liñas temporais + Permitir mostrar videos incrustrados directamente en liñas temporais + Permitir reverter o xeito para ler estados que se mostran unha vez pulsado o botón obter máis + Esta opción dalle soporte a recentes programas de cifra. É útil para dispositivos con versións antigas de Android o se non pode conectar coa instancia. + Exclusivo de vídeos Peertube. Cambie este modo se non pode velos. + Estas etiquetas permitiranlle filtrar estados dos perfís. Poderá utilizar o menú contextual para velas. + Inserta automáticamente un salto de liña tras a mención para por en maiúscula a primeira letra + Permitir as creadoras de contido compartir estados desde as súas fontes RSS + Redactar + Máx. número de intentos ao subir medios + Crear novo Cartafol aquí + Nome do cartafol + Por favor, introduza un nome válido + Este cartafol xa existe.\n Por favor, escriba outro nome para o cartafol + Seleccionar + Directorio por omisión + Cartafol + Crear cartafol + Mostrar mensaxe amigable tras terse completado unha acción (promo, fav, etc.)? + Exportáronse as instancias acaladas! + Engadir unha instancia + Exportar instancias + Importar instancias + Informes de fallos + Activar informes de fallos + Se se activa, crearase un informe local sobre o fallo que despois poderá compartir. + Fedilab detívose :( + Pode enviarme un correo-e co informe do fallo. Axudará a arranxalo :)\n\nPode engadir contido adicional. Grazas! + Utilizar o wysiwyg + Ao activalo, poderá darlle formato fácilmente coas ferramentas. + Estatísticas + Total de estados + Número de promocións + Número de favoritos + Número de mencións + Número de seguimentos + Número de sondaxes + Número de respostas + Número de estados + Estados + Visibilidade + Número con medios + Número con medios sensibles + Número con CW + Data do primeiro estado + Data do último estado + Data da primeira notificación + Data da última notificación + Frecuencia + %s de estados por día + %s notificacións por día + Rango de datas + Grupos + Sen grupos! + Desactivar emojis animados personalizados + Gráficos + Mostrar gráficos + A aplicación recolle os seus datos locais, agarde por favor... + Copia de respaldo + Estado do respaldo automático + Actívase para cada conta. Lanzará un servizo que gardará de xeito automático os seus estados localmente na base de datos. Esto permítelle obter estatísticas e gráficas + Respaldo automático notificacións + Esta opción é por conta. Iniciará un servio que gardará automáticamente as súas notificacións na base de datos local. Así poderá ter estatísticas e gráficos + Informar sobre a conta + Envíar un convite + A súa instancia non permite o rexistro de novas contas! + + %d voto + %d votos + + + %d votante + %d votantes + + + Opción única + Selección múltiple + + + 5 minutos + 30 minutos + 1 hora + 6 horas + 1 día + 3 días + 7 días + + + Torrent + Vista web + + Para unirse a miña instancia \"%1$s\", podes descargar Fedilab:\n\nF-Droid: %2$s\nGoogle: %3$s\n\nE despois abrir a ligazón inferior con Fedilab e crear a súa conta: :)\n\n%4$s + + A sondaxe non pode ter opición duplicadas! + Para todas as contas + Caché da base de datos + Limpar a caché da liña temporal de inicio + Eliminar os estados da caché + Eliminar marcadores + Ficheiros na caché + Total de notificacións + Agochar elementos do menú + As notificacións ao vivo están activadas + Para %1$s contas con %2$s eventos + Notificacións ao vivo para %1$s + Activaranse as notificacións ao vivo para esta conta. + Limpar caché ao saír + A caché (medios, menxases almacenadas, datos do navegador incluído) borraranse automáticamente ao saír da aplicación. + Queres deixar de seguir esta conta? + Mostrar cadro de confirmación antes de deixar de seguir + Substituír YouTube con Invidio.us + Invidious é unha interface de usuaria alternativa para YouTube + Introduce o teu servidor personalizado ou deixa baldeiro para usar invidio.us + Substituír Twitter con Nitter + Nitter é unha alternativa de código aberto a interface Twitter centrada na intimidade. + Introduza o seu servidor personalizado ou deixe baldeiro para utilizar nitter.net + Substituír Instagram con Bibliogram + Bibliogram é unha alternativa de código aberto centrada na privacidade a interface de Instagram. + Escribe o servidor personalizado ou deixa en branco para usar bibliogram.art + Substituir Reddit por Libreddit + Libreddit é unha alternativa de código aberto á interface de Reddit centrada na privacidade. + Escribe o teu servidor personalizado ou deixa baleiro para usar libredd.it + Substituír ligazóns a Medium + Substituír as ligazóns a medium.com cunha interface de código aberto alternativa centrada na privacidade. + Por defecto: scribe.rip + Substituír ligazóns a Wikipedia + Substituír ligazóns a Wikipedia cunha interface alternativa de código aberto centrada na privacidade. + Por defecto: wikiless.org + Ocultar barra de notificacións de Fedilab + Para ocultar as notificacións remanentes na barra de estado, pulsa na icona do ollo e desmarca: \"Mostrar en barra de estado\" + Usar un sistema de notificacións push para ter notificacións en tempo real. + Sen notificacións ao vivo + Notificacións ao vivo + Recolleranse as notificacións cada 15 minutos. + Engadir notas + Notas para a conta + Permitir comprimir imaxes grandes a un tamaño menor con perda mínima de calidade. + Permitir que se compriman os vídeos mantendo a calidade. + A app está comprimindo os medios, podería levarlle un anaco… + Cambiar icona da app + Preme para cambiar a icona + Publicación + Visibilidade da publicación + Preme para engadir fotos + Formatos admitidos: jpeg, png, gif \n\nTamaño Máx.: 15 MB \n\nOs álbumes poder ter ate 4 fotos ou vídeos + Subir medios + Engadir lenda de xeito optativo + A app recibeu un mensaxe de fallo moi longa desde API %1$s + Vista previa + Engadir mencións en cada mensaxe + Obtendo conversa + Orde por + Título para o vídeo + Unirse a Peertube + Teño 16 anos ao menos e acepto os %1$s de esta instancia + Ligazóns + Cambiar cor das ligazóns (URLs, mencións, etiquetas, etc.) nas mensaxes + Cabeceira das repeticións + Cambiar cor do nome mostrado enriba das mensaxes + Cambiar a cor do nome de usuaria enriba das mensaxes + Cambiar a cor da cabeceira das mensaxes repetidas + Publicacións + Cor de fondo das publicacións en liñas temporais + Restablecer cores + Premendo aquí restableces os valores por omisión + Restablecer + Iconas + Cor das iconas inferiores nas liñas temporais + Fixar esta etiqueta + Logo da instancia + Editar perfil + Tomar decisión + Tradución + Vista previa da imaxe + Cor do texto + Cambiar cor do texto nas mensaxes + Aplicar cambios + Debes reiniciar para aplicar os cambios + Reiniciar + Usar decorado personalizado + Permitir obviar as cores do decorado seleccionado + Decoración + Gardar antes + Exportouse o decorado + Exportouse correctamente o decorado a CSV + Aplicar a cor primaria a barra de estado + Cor da barra de estado + Restablecer decorado por omisión + Importar decorado + Toca aquí para importar un decorado previamente exportado + Exportar decorado + Toca aquí para exportar decorado actual + Algo fallou ao escoller o ficheiro do decorado + Selección de decorado + Escoller decorado preinstaldo + Decorados + Aplicar cor primaria a barra de navegación + Cor da barra de navegación + A cor de fondo do contido da app. + Cor de fondo + Resalta partes seleccionadas da IU. + Cor de resalte + Mostrado frecuentemente a través da app. + Cor primaria + Exportar marcadores a instancia + Importar marcadores desde a instancia + Número de usuarias + Número de estados + Contas da instancia + Bloqueado + Remata en %s + Qué novidades hai en %s + Podes seguir a miña conta para actualizacións + Esta instancia non está dispoñible en https://instances.social + Mostra ligazón completa + Comparte ligazón + Copiouse o URL ao portaretallos + Abrir con outra app + Comproba redirección + Este URL non redirixe + %1$s \n\nredirixe a\n\n %2$s + Cambiar User Agent + Establecer un User Agent ou deixar baldeiro + Permite personalizar o User Agent utilizado nas chamadas a API ou co navegador incluído. + Eliminar parámetros UTM + A app borrará automáticamente os parámetros UTM dos URLs antes de visitar a ligazón. + Tendencias + Tendencia agora + %d xente comentando + Contas de Twitter (vía Nitter) + Nomes de usuaria Twitter separados por vírgulas + Probas de identidade + Identidade verificada + Verificada por %1$s (%2$s) + Borrar a notificación + Mostrar máis opcións + É unha Historia en Pixelfed + Subir medios, engadirase automaticamente a Historia en Pixelfed. + Medios engadidos correctamente a Historia! + Acción desactivada + Deixar de seguir + Algo fallou, comproba o directorio de descarga nos axustes. + Anuncios + Sen anuncios! + Engadir reacción + Usa o teu navegador favorito dentro da app. Desmarca esta opción para abrir externamente as ligazóns. + Caché para vídeo en MB, cero significa sen caché. + Marcas de auga + Engadir marcas de auga automáticamente ás imaxes. O texto pódese personalizar para cada conta. + Non se atopan distribuidores! + Precisas un distribuidor para recibir notificacións push.\nAtoparás máis detalles en %1$s.\n\nTamén podes desactivar as notificacións push nos axustes para ignorar esa mensaxe. + Elixe un distribuidor + diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml new file mode 100644 index 000000000..c6c3852f6 --- /dev/null +++ b/app/src/main/res/values-hi/strings.xml @@ -0,0 +1,1134 @@ + + + मेन्यू खोलें + मेन्यू बंद करें + के बारे में + इंस्टंस के बारे में + निजता + कैश + लॉग आउट + प्रवेश + + बंद करें + हाँ + नहीं + रद्द करें + डाउन्लोड + डाउन्लोड %1$s + मीडिया संचित कर लिया गया है + फ़ाइल: %1$s + पासवर्ड + ईमेल + खाते + लेख + टैग्स + सहेजें + पहले जैसा करें + कोई परिणाम नहीं! + इंस्टंस + इंस्टंस: mastodon.social + अब इस खाते के साथ काम करता है %1$s + खाता जोड़ें + इस संदेश के विषय वस्तु की प्रतिलिपि को क्लिपबोर्ड में छाप दिया गया है + The URL of the toot has been copied to the clipboard + बदलें + एक चित्र चुनें + साफ करें + कैमरा + सब हटाएँ + इस संदेश का अनुवाद करें + शैड्यूल + अक्षर और आइकाॅन का आकार + अक्षरों के वतर्मान आकार को बदलें: + अाइकाॅन के वर्तमान आकार को बदलें: + अगला + पिछला + के साथ खोलें + स्वीकार + मीडिया + के साथ सांझे + मैस्टालैब के द्वारा सांझा + जवाब + उपयोगकर्ता का नाम + मसौदे + पसंदीदा + नए अनुयायी + उल्लेख + बूस्ट + बूस्ट दिखाए + जवाबों को दिखाए + ब्राउज़र में खोलें + अनुवाद करें + इस क्रिया को करने से पहले कृपया कुछ सेकेंड ठहरे। + + मुख्यपृष्ठ + लोकल टाइम्लाइन + फ़ेडरेटेड टाइम्लाइन + विकल्प + पसंदीदा + संचार + मौन किये गये उप्योगकर्ता + प्रतिबंधित उप्योगकर्ता + सूचनाएँ + अनुसरण करने की गुज़ारिशें + सैटिंग्स + खाता नष्ट करें + %1$s खाते को एप्लीकेशन से हटाएं? + ईमेल भेजें + मार्ग को बदलने के लिये उस पर क्लिक कीजिये + असफल! + परिगणित लेख + नीचे दिये गये जानकारी उप्योगकर्ता का वर्णन अधूरा दर्शा सकता है। + इमोजी डाले + इस वक्त एप ने कस्टम इमोजियों को एकत्र नहीं किया। + Push notifications + Are you sure you want to logout? + Are you sure you want to logout @%1$s@%2$s? + + दिखाने के लिये एक भी संदेश नहीं है + No stories to display + Stories + बूस्ट किया %1$s + इस संदेश को अपने फ़ेवरेट्स में जोड़े? + इस संदेश को अपने फ़ेवरेट्स से हटाएं? + इस संदेश को बूस्ट करें? + इस संदेश को अंबूस्ट करें? + इस संदेश को जड़ दे? + इस संदेश को अजड़ दे? + मौन करें + अवरुद्ध करें + रिपोर्ट + हटाएं + प्रतिलिपि + सांझा करें + उल्लेख + समयबद्ध मौन करें + हटाएं & दोबारा लिखें + + इस खाते को मौन करें? + इस खाते को अवरुद्ध करें? + इस संदेश को रिपोर्ट करें? + इस इंस्टेंस को अवरुद्ध करें? + इस खाते को मौन करें? + Unblock this account? + + + सूचित करें + मौन + + + इस संदेश को हटाएं? + संदेश हटाएं और दोबारा लिखे? + + बुकमार्क्स + बुकमार्क्स में जोड़ें + बुकमार्क हटाएं + कोई बुकमार्क प्रदर्शित करने के लिये नहीं है + संदेश को बुकमार्क्स में जोड़ दिया गया है! + संदेश को बुकमार्क्स से हटा दिया गया है! + + %d s + %d m + %d h + %d d + + %d second + %d seconds + + + %d minute + %d minutes + + + %d hour + %d hours + + + %d day + %d days + + + चेतावनी + आपके मन में क्या है? + भेजें! + QUEET! + cw + संदेश लिखे + संदेश का जवाब दे + Write a queet + Reply to a queet + मीडिया को चुनें + मीडिया चुनते वक्त कुछ गड़बड़ी हो गई! + इस मीडिया को मिटा दें? + आपने कुछ भी नहीं लिखा है! + संदेश की दृश्यता + लेखो की दृश्यता का पूर्व निर्धारित रूप: + इस संदेश को भेज दिया गया है! + आप इस संदेश का जवाब दे रहे हैं: + संवेदनशील विषय वस्तु? + + सार्वजनिक टाइम्लाइन पर भेजें + सार्वजनिक टाइम्लाइन पर मत भेजिए + केवल अनुयायियो को भेजें + केवल अंकित उपयोगकर्ताओं को भेजें + + कोई मसौदा नहीं है! + संदेश चुनें + खाता चुनें + कुछ खातो को चुनें + मसौदा हटाएं? + प्रारंभिक संदेश को दिखाने के लिये बटन पर क्लिक करें + नेत्रहीन लोगों के लिये वर्णन दे + + कोई वर्णन उपलब्ध नहीं है! + + रिलीज़ %1$s + डैवलपर: + लाइसेंस: + जीएनयू जीपीएल वी३ + सोर्से कोड: + लेखों का अनुवाद: + इंस्टंस को खोजें: + आइकाॅन डिज़ाइनर: + + बातचीत + + कोई खाता मौजूद नहीं है + अनुसरण करने की गुज़ारिश नहीं है + लेख \n %1$s + अनुसरण \n %1$s + अनुसरणकर्ता \n %1$s + जड़ा \n %d + अधिकृत करें + अस्वीकार + + कोई परिगणित लेख मौजूद नहीं है! + लेख लिखने के बाद ऊपरी मेन्यू में से शैड्यूल को चुनें। + परिगणित लेख मिटाएं? + मीडिया: %d + लेख को परिगणित कर दिया! + परिगणित तारीक वर्तमान घंटे से आगे होना चाहिये। + बैटरी बचत चालू है! उम्मीदों के मुताबिक शायद काम ना करें। + + मौन करने का समय एक मिनट से अधिक होना चाहिये। + %1$s को खामोश कर दिया गया है %2$s तक।\n आप इस खाते का उसके प्रोफ़ाइल पेज से फिर से चालू कर सकते हैं। + %1$s को %2$s तक खामोश कर दिया है।\n चालू करने के लिये यहाँ क्लिक करें। + + कोई सूचना मौजूद नहीं है + आपका उल्लेख किया + wrote a new message + आपका लेख बूस्ट किया + आपका लेख पसंद किया + आपका अनुसरण किया + asked to follow you + + एक और सूचना + %d अन्य सूचनाएं + + + %d like + %d likes + + सूचना मिटाएं? + सभी सूचनाओ को मिटाएं? + सूचना को मिटा दिया गया है! + सभी सूचनाएं मिटा दिए गये है! + + अनुसरण + अनुसरणकर्ता + जड़े हुए + + क्लाइंट आईडी नहीं मिल सका! + Unable to connect to instance domain! + इंटरनेट कनेक्शन नहीं है! + इस खाते को अवरुद्ध कर दिया! + खाता अब अवरुद्ध नहीं है! + खाते को खामोश कर दिया! + खाता अब खामोश नहीं है! + इस खाते का अनुसरण कर लिया! + खाते का अनुसरण करना बंद कर दिया! + लेख को बूस्ट कर दिया! + लेख से बूस्ट हटा दिया! + लेख को आपके पसंदीदा लेखों में जोड़ दिया! + लेख को आपके पसंदीदा लेखों में से हटा दिया! + लेख को रिपोर्ट कर दिया! + लेख को मिटा दिया! + लेख को जड़ दिया! + लेख अब जड़ा हुआ नहीं है! + अरे ! कुछ गड़बड़ी हो गयी! + एक गड़बड़ी हो गयी! इंस्टंस ने प्राधिकरण कोड वापस नहीं दिया! + यह इंस्टंस डोमेन वैध नहीं लग रहा! + खाता बदलते समय कुछ गड़बड़ी हो गयी! + खोजते समय कुछ गड़बड़ी हुई! + प्रोफ़ाइल डेटा सहेजा गया! + कार्यवाही नामुमकिन + मीडिया को सहेज लिया! + अनुवाद करते समय गड़बड़ी हुई! + Translations are disabled in settings + मसौदा सहेजा! + क्या आप सुनिश्चित है कि यह इंस्टंस इतने सारे अक्षर स्वीकार करता है? आम तौर पर यह मूल्य ५०० अक्षरों के करीब होता है। + %1$s खाते के लेखों की दृश्यता को बदल दिया गया है + + लेखों की संख्या प्रति लोड + हमेशा + वाईफ़ाई + पूछें + मीडिया को लोड करें + चित्रो को लोड करें + और दिखाएं + कम दिखाएं + संवेदनशील विषय वस्तु + गिफ़ अवतार निष्क्रिय करें + मार्ग: + मसौदे स्वतः सहेजें + मीडिया के यूआरएल को लेखों में जोड़े + अनुसरण होने पर सूचित करें + आपके लेख बूस्ट होने पर सूचित करें + आपके लेख पसंद होने पर सूचित करें + आपका उल्लेख होने पर सूचित करें + Notify when a poll ended + Notify for new posts + बूस्ट करने से पहले पूछें + पसंद करने से पहले पूछें + केवल वाईफ़ाई पे सूचित करें + सूचित करें? + शांत सूचनाएं + NSFW दृश्य विराम (सेकेंड, ० मतलब बंद) + Media Description timeout (seconds, 0 means off) + प्रोफ़ाइल संपादित करें + Custom sharing + Your custom sharing URL… + बायो... + खाता लाॅक करें + बदलाव सहेजें + एक हैडर चित्र चुनें + छवि पूर्वदर्शन को फ़िट करें + जवाबो में ५०० अक्षरो से ज़्यादा वाले लेखो को स्वतः अलग करें + आप अनुमत १६० अक्षरो की सीमा तक पहुंच चुके हैं! + आप अनुमत ३० अक्षरो की सीमा तक पहुंच चुके हैं! + के बीच + और + समय %1$s से अधिक होना चाहिये + समय %1$s से कम होना चाहिये + आरंभ समय + अंतिम समय + आंतरिक ब्राउज़र का उपयोग करें + कस्टम टैब्स + जावास्क्रिप्ट चालू करें + स्वतः cw खोलें + तीसरी पार्टी कुकीज़ को स्वीकारें + आपकी एपीआई कुंजी, आप इसे यैंडेक्स के लिये खाली छोड़ सकते हैं + + साँवला + हल्का + काला + + एलईडी का रंग चुनें: + + नीला + हरिनील + लाल बैंगनी + हरा + लाल + पीला + सफेद + + अनुसरण करें + मुक्त करें + मौन करें + मौन तोड़ें + अनुरोध भेज दिया + आपका अनुसरण कर रहे हैं + खोजें + जवाबों में पहले अक्षर को बड़ा रखें + चित्रो का नाप बदलें + Resize videos + + पुश सूचनाएँ + कृप्या उन पुश सूचनाएं को पक्का करें जो आप पाना चाहते हैं। + +आप सूचनाओं को बाद में सेटिंग्स में जाकर सक्रिय या निश्क्रिय कर सकते हैं (सूचनाएं टैब)। + + कैश साफ़ करें + कैश में %1$s डेटा है। \n\nक्या आप उन्हें मिटाना चाहेंगे? + एमबी + कैश को साफ़ कर दिया! %1$s मुक्त + + शीर्षक + शीर्षक… + विवरण + Keywords + Keywords… + + समक्रमण करें + छांटे + आपके लेख + Your notifications + सार्वजनिक + असूचीबद्ध + निजी + सीधा + कुछ कुंजी शब्द... + मीडिया दिखाएं + जड़े हुए दिखाएं + कोई मिलता-जुलता परिणाम नहीं मिला! + %1$s के लेखों को बैकअप करें + %1$s नये लेख आयात कर लिये गये है + %1$s new notifications have been imported + + Dates descending + Dates ascending + + + नहीं + केवल + दोनों + + डेटाबेस में कोई लेख नहीं मिला। उन्हें वापस पाने के लिये मेन्यू में मौजूद सिंक बटन का इस्तेमाल करें। + + अभिलिखित डेटा + खातों की सिर्फ़ साधारण जानकारी उपकरण में रखी जाती है। +यह डेटा बिलकुल गोपनीय है और इसका उपयोग सिर्फ़ यह एेप कर सकता है। +एेप के मिटाते ही यह डेटा तुरंत नष्ट हो जाता है।\n +⚠ लाॅगइन और पासवर्ड कभी रखे नहीं जाते। उनका उपयोग केवल किसी इंस्टंस के साथ सुरक्षित प्रमाणीकरण (SSL) के दौरान किया जाता है। + अनुमतियां: + - एेकस्स_नैटवर्क_स्टेट: यह पता लगाने के लिये कि उपकरण वाईफाई से जुड़ा है या नहीं।\n +- इंटर्नेट: इंस्टंस से परिप्रश्न करने के लिये।\n +- राइट_एेक्सटर्नल_स्टोरेज: मीडिया संचित या एेप को एेसडी कार्ड में डालने के लिये।\n +- पढ़े_एेक्सटर्नल_स्टोरेज: लेखों में मीडिया जोड़ने के लिये।\n +- बूट_समाप्त: सूचना सेवा शुरू करने के लिये।\n +- वेक_लौक: सूचना सेवा के दौरान उपयोग करने के लिये। + एपीआई अनुमतियां: + - पढ़ें: डेटा पढ़ें।\n +- लिखें: लेख प्रकाशित करें और मीडिया अपलोड करें।\n +- अनुसरण: अनुसरण, अंफ़ौलो, अवरुद्ध, मुक्त।\n\n +- ⚠ केवल उपयोगकर्ता की गुज़ारिश पर यह क्रियाएं की जाती हैं। + ट्रैकिंग एवं संग्रहालय + यह एेप ट्रैकिंग साधन का उपयोग नहीं करता (श्रोता मापन, त्रुटि रिपोर्ट आदी) और इसमें कोई विज्ञापन नहीं है।\n\n +संग्रहालयो के उपयोग कम किया है: \n +- ग्लाइड: मीडिया को संभालने के लिये\n +- एंड्राॅयड-जाॅब: सेवाएं संभालने के लिये\n +- फ़ोटोव्यू: चित्रो को संभालने के लिये\n + लेखों का अनुवाद + यह एेप लेखों का अनुवाद करने की काबिलियत प्रदान करता है उपकरण का स्थान और यैंडैक्स एपीआई की मदद से।\n +यैंडैक्स की अपनी गोपनीयता नीति है जो यहाँ मौजूद है: https://yandex.ru/legal/confidential/?lang=en + इनको धन्यवाद: + आम अभिव्यंजनाओ के अनुसार छाटें + खोजें + मिटाएं + और लेख खोजिये... + + सूचियां + क्या आप वाकई इस सूची को हमेशा के लिये मिटाना चाहते हैं? + यह सूची फिलहाल खाली है। जब इस सूची के सदस्य नये लेख प्रकाशित करेंगे तब वे इधर दिखेंगे। + सूची में जोड़ें + सूची जोड़ें + सूची मिटाएं + सूची संपादित करें + नये सूची का शीर्षक + The account was added to the list! + You don\'t have any lists yet! + + %1$s %2$s पे चले गये है + प्रमाणीकरण काम नहीं कर रहा? + यह कुछ चैक्स हैं जो मदद करें:\n\n +- जांचे कि इंस्टंस के नाम में कोई गलती तो नहीं ह\n\n +- जांचे की आपका इंस्टंस कहीं बंद तो नहीं है\n\n +- अगर आप दो-चरणीय प्रमाणीकरण (2FA) का उपयोग करते है तो कृप्या नीचे दिये लिंक का इस्तेमाल करें (इंस्टंस का नाम डालने के बाद)\n\n +- आप इस लिंक का उपयोग दो-चरणीय प्रमाणीकरण के बिना भी कर सकते हैं\n\n +- अगर यह फिर भी काम नहीं कर रहा तो कृप्या गिटलैब पे रिपोर्ट करें https://gitlab.com/tom79/mastalab/issues + मीडिया को लोड कर दिया है। देखने के लिये यहाँ क्लिक करें। + यह क्रिया काफी लंबी हो सकती है। पूरा होने पे आपको सूचित कर दिया जाएगा। + चालू है, कृप्या प्रतीक्षा करे... + लेखों को निर्यात करें + %1$s के लिये लेखों को निर्यात करें + %2$s लेखो में से %1$s लेखों का आयात कर लिया। + %1$s के लिये डेटा निर्यात करते समय गड़बड़ी हो गयी + Something went wrong when exporting data! + Something went wrong when importing data! + + प्रॉक्सी + प्राॅक्सी चालू करें? + मेज़बान + पोर्ट + लॉगइन + पासवर्ड + लेख को सांझा करते समय उसकी जानकारी जोड़ें + लीबरापे पे एेप का समर्थन करें + आम अभिव्यंजना में एक गड़बड़ी है! + इस इंस्टंस पे कोई टाइम्लाइन नहीं मिली! + इस इंस्टंस को मिटाएं? + में अनुवाद करें + इंस्टंस का अनुसरण करें + आप इस इंस्टंस का पहले से अनुसरण कर रहे हैं! + इंस्टंस का अनुसरण किया! + साझेदारियां + जानकारी + %s के बूस्ट छुपाएं + प्रोफ़ाइल पे पेश करें + %s के बूस्ट दिखाएं + प्रोफ़ाइल पे पेश ना करें + खाता अब प्रोफ़ाइल पे पेश है + खाता अब प्रोफ़ाइल पे पेश नहीं है + बूस्ट अब प्रस्तुत होंगे! + बूस्ट अब प्रस्तुत नहीं होंगे! + सीधा संदेश + छन्नियां + कोई छन्नी मौजूद नहीं है। आप \"+\" बटन को क्लिक करके एक छन्नी बना सकते हैं। + कुंजी शब्द या वाक्यांश + मुख्यपृष्ट + सार्वजनिक टाइम्लाइन + सूचनाएं + बातचीत + अक्षरो के आकार या लेख के विषय वस्तु चेतावनी के बावजूद जोड़ा जाएगा + छिपाने के बजाय हटा दें + छनें हुए लेख हमेशा के लिये मिट जाएंगे, छन्नी को बाद में हटाने के बावजूद + जब कुंजी शब्द या वाक्यांश सिर्फ़ अक्षरांकीय है तब पूरे शब्द के साथ बराबर मिलने पर ही लागू होगा + पूरा शब्द + संदर्भ छाटें + एक या अनेक संदर्भ जहाँ छन्नी लागू हो + के बाद खत्म हो + छन्नी मिटाएं? + छन्नी का नवीनीकरण करें + छन्नी बनाएं + किनका अनुसरण करें + इस समय कोई खाता सूचीबद्ध नहीं है! + अनुसरण करें + सभी चुनें + सभी अचयनित करें + %s का अनुसरण किया! + %s सूची बना रहे हैं + खोतों को सूची में जोड़ा जा रहा है + खातों को सूची में जोड़ दिया + खोतों को सूची में जोड़ जा रहा है + आपने कोई सूची नहीं बनाई है। एक नयी सूची बनाने के लिये \"+\" बटन पे क्लिक करें। + किसका अनुसरण करें + ट्रंक एपीआई + खातों का अनुसरण नहीं हो पाया + दूरर्वती खाता लाया जा रहा है + स्वतः छुपाए गये मिडिया को दिखाएं + नया अनुसरण + नया बूस्ट + नया पसंदीदा + नया उल्लेख + चुनाव समाप्त हो गया + नया लेख + लेखों का बैकअप + New posts + मीडिया डाउंलोड + सूचना संगीत को बदलें + टोन चुनें + निर्धारित समय चालू करें + शिक्षण वीडियो + दूरवर्ती धागे को लाया जा रहा हा! + कोई अवरुद्ध डोमेन नहीं है! + डोमेन मुक्त करें + क्या आपको वाकई %s को मुक्त करना है? + क्या आपको वाकई %s को अवरुद्ध करना है? + अवरुद्ध किये डोमेन + डोमेन अवरुद्ध करें + डोमेन अवरुद्ध है + यह डोमेन अब अवरुद्ध नहीं है! + दूरर्वती लेखों को लाया जा रहा है + टिप्पणी + पीयरट्यूब इंस्टंस + वीडियो पे सबसे पहले टिप्पणी करने वाले बने ऊपरी दाई ओर वाले बटन के साथ! + %s बार देखा + अवधि: %s + एक इंस्टंस जोड़ें + इस वीडियो पर टिप्पणियां निष्क्रिय हैं! + एक रेज़लूशन चुनें + पीयरट्यूब पसंदीदा + इस वीडियो को बुकमार्क्स में जोड़ दिया गया है! + इस वीडियो को बुकमार्क्स से हटा दिया गया है! + आपके पसंदीदा सूची में कोई पीयरट्यूब वीडियो नहीं है! + चैनल + वीडियो + चैनल + इमोजी वन इस्तेमाल करें + जानकारी + सभी लेखो में पूर्वदर्शन लागू करें + नया UX/UI डिज़ाइनर + वीडियो के पूर्वदर्शन छवियां दिखाएं + खाते की आईडी की प्रतिलिपि क्लिपबोर्ड में छाप दी है! + भाषा बदलें + पूर्वनिर्धारित भाषा + लंबे लेखों को छोटा करें + \'x\' पंक्तियों से लंबे लेखो को छोटा करें। शून्य मतलब निष्क्रिय। + अधिक दिखाएं + कम दिखाएं + टैग्स संभाले + टैग पहले से मौजूद है! + टैग को सहेज लिया! + टैग को बदल दिया! + टैग को मिटा दिया! + बूस्ट शैड्यूल करें + बूस्ट परिगणित हो गया! + कोई परिगणित बूस्ट मौजूद नहीं है! + बूस्ट शैड्यूल करें।]]> + कला टाइम्लाइन + मेन्यू खोलें + वापस जाएं + एेप का चिह्न + प्रोफ़ाइल चित्र + प्रोफ़ाइल बैनर + इंस्टंस के प्रबंधक से संपर्क करें + नया जोड़ें + मैस्टोहोस्ट का चिह्न + इमोजी चयनकर्ता + नवीकरण + बातचीत को खोलें + एक खाता हटाएं + अवरुद्ध किये डोमेन को मिटाएं + कस्टम इमोजी चयनकर्ता + वीडियो चलाएं + नया लेख + पत्ते की छवी + मीडिया छुपाएं + फ़ेवीकाॅन + दृष्टिहीन लोगों के लिये मीडिया का विवरण दें + + कभी नहीं + ३० मिनट + १ घंटा + ६ घंटे + १२ घंटे + १ दिन + १ सप्ताह + + यहाँ अपने इंस्टंस के मेज़बान का नाम डालें।\nउदाहरण, अगर आपने https://mastodon.social\n पे खाता बनाया है तो बस लिखें mastodon.social (https:// के बिना)\n +आप जब पहले अक्षर लिखना शुरू करेंगे तब नामों का सुझाव दिया जाएगा।\n\n +⚠ लाॅगइन बटन सिर्फ़ तब काम करेगा जब इंसटंस का नाम वैध है और इंस्टंस काम कर रहा है! + अधिक जानकारी + + भाषाएँ + सिर्फ़ मीडिया + NSFW दिखाएं + क्राउडिन के अनुवाद + क्राउडिन प्रबंधक + एेप का अनुवाद + क्राउडिन के बारे में + बॉट + पिक्सलफ़ैड इंस्टंस + मैस्टोडाॅन इंस्टंस + इनमें से कोई भी + यह सभी + कोई भी नहीं + कोई भी शब्द (स्पेस से अलग किये हूए) + सभी शब्द (स्पेस से अलग किये हूए) + Add some words to filter (space-separated) + काॅलम क नाम बदलें + Misskey instance + No app supporting this link is installed on your device. + सदस्यता + Overview + चर्चित + हाल ही में जोडा हुआ + स्थानीय + अपलोड + उत्तर + टिप्पणी हटाएँ + क्या आप सचमुच इस टिप्पणी को हटाना चाहते हैं? + पूर्ण स्क्रीन वीडियो + वीडियो के लिए मोड + अपलोड करने के लिए फ़ाइल का चयन करें + मेरे वीडियो + शीर्षक: + लाइसेंस + वर्ग + भाषा + इस वीडियो में परिपक्व या स्पष्ट सामग्री है + वीडियो टिप्पणियों को सक्षम करें + वीडियो अपडेट करें + विवरण + The video has been updated! + अपलोड रद्द कर दिया गया + वीडियो अपलोड हो गया है + Uploading, please wait… + Tap here to edit the video data. + वीडियो हटाएँ + Are you sure to delete this video? + Display NSFW videos + No videos to display! + कोई टिप्पणी लिखें + शेयर करें + चुनें एक कार्यक्रम मोड + डिवाइस से + सर्वर से + Toots (Server) + Toots (Device) + परिवर्तित करें + Display new toots above the \"Fetch more\" button + समयरेखा + Interface + संपर्क सूची + %1$s commented your video %2$s]]> + %1$s is following your channel %2$s]]> + %1$s is following your account]]> + %1$s has been published]]> + %1$s succeeded]]> + %1$s failed]]> + %1$s published a new video: %2$s]]> + %1$s has been blacklisted]]> + %1$s has been unblacklisted]]> + Export data + Import Data + Select the file to import + An error occurred when selecting the backup file! + एक सार्वजनिक टिप्पणी जोड़ें + टिप्पणी भेजे + There is no Internet connection. Your message has been stored in drafts. + साधारण शब्द + HTML + मार्कडाउन + खाते को लॉगआउट करे + All + Support the app + Open Collective enables groups to quickly set up a collective, raise funds and manage them transparently. + लिंक कॉपी करें + कनेक्ट करें + साधारण + Compact + कंसोल + Set display mode + Patch the Security Provider + Update tracking domains + The tracking data base has been updated! + http calls blocked by the application + List of blocked calls + सबमिट करें + The data base has been exported! + Featured hashtags + Filter timeline with tags + No tags + Hide the \"delete\" button in the notification tab + Attach an image when sharing a URL + + मतदान + मतदान + Create a poll + विकल्प 1 + विकल्प 2 + विकल्प %d + You need two choices at least for the poll! + समाप्त + end at %s + Refresh poll + मत + A poll you have voted in has ended + A poll you tooted has ended + Customize + श्रेणियाँ + Time slot + Advanced + Display \'new\' badge on unread toots + Peertube + Move timeline + Hide timeline + Reorder timelines + List permanently deleted + Followed instance removed + Pinned tag removed + Undo + You need to keep two visible tabs! + Reorder timelines + Main timelines can only be hidden! + BBCode + Always mark media as sensitive + GNU instance + Cached status + Forward tags in replies + Long press to store media + Blur sensitive media + Display timelines in a list + Display timelines + Mark bot accounts in toots + Manage tags + Remember the position in Home timeline + इतिहास + गानो की सूची + प्रदर्शित होने वाला नाम + You don\'t have any playlists. Tap on the \"+\" icon to add a new playlist + You must provide a display name! + The channel is required when the playlist is public. + नई प्लेलिस्ट बनाएं + There is nothing in this playlist yet. + फिर से करें + गैलरी + इमोजी + स्टीकर + रबड़ + शब्द + फ़िल्टर + ब्रश + Are you sure you want to exit without saving the image? + रद्द करें + Saving… + Image Saved Successfully! + Failed to save Image + अपारदर्शिता + Enable photo editor + Add a poll item + Remove last poll item + Mute conversation + Unmute conversation + The conversation is no longer muted! + The conversation is muted + Open application features + Timed mute + Mention the account + Refresh cache + Mention the status + समाचार + सामान्य + Regional + कला + पत्रकारिता + Activism + गेमिंग + तकनीकी + Adult content + Furry + खाना + Logo of the instance + Something went wrong when checking available instances! + Join Mastodon + Choose an instance by picking up a category, then tap on a check button. + Choose an instance by tapping on a check button. + %1$s उपयोगकर्ता + पासवर्ड की पुष्टि करें + मैं %1$s और %2$s से सहमत हूँ + server rules + सेवा की शर्तें + साइन अप करें + This instance works with invitations. Your account will need to be manually approved by an administrator before being usable. + कृपया सभी जानकारियां को भरें! + Passwords don\'t match! + The email doesn\'t seem to be valid! + Your username will be unique on %1$s + You will be sent a confirmation e-mail + कम से कम ८ अक्षर का उपयोग करे + Password should contain at least 8 characters + Username should only contain letters, numbers and underscores + Account created! + Your account has been created!\n\n + Think to validate your email within the 48 next hours.\n\n + You can now connect your account by writing %1$s in the first field and click on Connect.\n\n + Important: If your instance required validation, you will receive an email once it is validated! + + Save the message in drafts? + Administration + Reports + No reports to display! + Reconnect the account + The application failed to access the admin features. You might need to reconnect the account for having the correct scope. + Unresolved + Remote + Active + Pending + Disabled + Silenced + Suspended + Permissions + Email status + Login status + Joined + Most recent IP + Warn + Disable + Silence + Notify the user per e-mail + Custom warning + User + Moderator + Administrator + Confirmed + Not confirmed + Reported statuses + Account + Undo silence + Undo disable + Suspend + Undo suspend + The account is silenced! + The account is no longer silenced! + The account is suspended! + The account is no longer suspended! + The account is disabled! + The account is no longer disabled! + The account has been warned! + Display the admin menu + Display the admin feature in statuses + Allow + The account is approved! + The account is rejected! + Assign to me + Unassign + Mark as resolved + Mark as unresolved + Empty content! + Display Fedilab features button + The application needs to access audio recording + Voice message + Enable quick reply + The account you are replying might not see your message! + If disabled, the app will always load last statuses + If disabled, sensitive media will be hidden with a button + Store media in full size with a long press on previews + Add an ellipse button at the top right for listing all tags/instances/lists + During the time slot, the app will send notifications. You can reverse (ie: silent) this time slot with the right spinner. + Display a Fedilab button below profile picture. It is a shortcut for accessing in-app features. + Allow to reply directly in timelines below statuses + Previews will not be cropped in timelines + Allow to play embedded videos directly in timelines + Allow to reverse the way to read statuses that are displayed once tapping the fetch more button + This option allows to support recent cipher suites. It is useful for older Android devices or if you cannot connect to your instance. + Exclusively for Peertube videos. Switch this mode if you cannot play them. + These tags will allow to filter statuses from profiles. You will have to use the context menu for seeing them. + Automatically insert a line break after the mention to capitalize the first letter + Allow content creators to share statuses to their RSS feeds + Compose + Maximum retry times when uploading media + Create a new Folder here + Enter the folder name + Please enter a valid folder name + This folder already exists.\n Please provide another name for the folder + Select + Default Directory + Folder + Create folder + Display a toast message after an action has been completed (boost, fav, etc.)? + Muted instances have been exported! + Add an instance + Export instances + Import instances + Crash reports + Enable crash reports + If enabled, a crash report will be created locally and then you will be able to share it. + मैस्टालैब रुक गया है :( + आप मुझे ईमेल के द्वारा क्रैश रिपोर्ट भेज सकते है। यह उसे ठीक करने में मदद देगी :)\n\nआप अतिरिक्त विषय वस्तु जोड़ सकते है। धन्यवाद! + Use the wysiwyg + When enabled, you will be able to format your text easily with tools. + Statistics + Total statuses + Number of boosts + Number of favourites + Number of mentions + Number of follows + Number of polls + Number of replies + Number of statuses + Statuses + Visibility + Number with media + Number with sensitive media + Number with CW + First status date + Last status date + First notification date + Last notification date + Frequency + %s statuses per day + %s notifications per day + Date range + Groups + No groups! + Disable custom animated emojis + Charts + Display charts + The application collects your local data, please wait… + Backup + Auto backup statuses + This option is per account. It will launch a service that will automatically store your statuses locally in the database. That allows to get statistics and charts + Auto backup notifications + This option is per account. It will launch a service that will automatically store your notifications locally in the database. That allows to get statistics and charts + Report account + Send an invitation + Your instance does not allow to register a new account! + + %d मत + %d मत + + + %d voter + %d voters + + + एक विकल्प + अनेक विकल्प + + + ५ मिनट + ३० मिनट + १ घंटा + ६ घंटे + १ दिन + ३ दिन + ७ दिन + + + वेबव्यू + Direct stream + + For joining my instance \"%1$s\", you can download Fedilab:\n\nF-Droid: %2$s\nGoogle: %3$s\n\nThen open the link below with Fedilab and create your account :)\n\n%4$s + + Your poll can\'t have duplicated options! + For all accounts + Database cache + Clear your home timeline cache + Clear your cached statuses + Clear your bookmarks + Files in cache + Total notifications + Hide menu items + Fedilab is running live notifications + For %1$s accounts with %2$s events + Live notifications for %1$s + Live notifications will be enabled for this account. + Clear cache when leaving + The cache (media, cached messages, data from the built-in browser) will be automatically cleared when leaving the application. + Do you want to unfollow this account? + Show confirmation dialog before unfollowing + Replace Youtube with Invidio.us + Invidious is an alternative front-end to YouTube + Enter your custom host or leave blank for using invidious.snopyta.org + Replace Twitter with Nitter + Nitter is an open source alternative Twitter front-end focused on privacy. + Enter your custom host or leave blank for using nitter.net + Replace Instagram with Bibliogram + Bibliogram is an open source alternative Instagram front-end focused on privacy. + Enter your custom host or leave blank for using bibliogram.art + Replace Reddit with Libreddit + Libreddit is an open source alternative Reddit front-end focused on privacy. + Enter your custom host or leave blank for using libredd.it + Replace Medium links + Replace medium.com links with an open source alternative front-end focused on privacy. + Default: scribe.rip + Replace Wikipedia links + Replace Wikipedia link with an open source alternative front-end focused on privacy. + Default: wikiless.org + Hide Fedilab notification bar + For hiding the remaining notification in the status bar, tap on the eye icon button then uncheck: \"Display in status bar\" + Use a push notifications system for getting notifications in real time. + No live notifications + Live notifications + Notifications will be fetched every 15 minutes. + Add notes + Notes for the account + Allow to compress large photos into smaller sized photos with very less or negligible loss in quality of the image. + Allow compressing videos while maintaining their quality. + The app is compressing the media, it can be quite long… + Change app icon + Tap to change the app icon + Post + Visibility of the post + Tap here to add photos + Accepted Formats: jpeg, png, gif \n\nMax File Size: 15 MB \n\nAlbums can contain up to 4 photos or videos + Upload media + Add an optional caption + The app received a very long error message from the API %1$s + Message preview + Add mentions in each message + Fetching conversation + Order by + Title for the video + Join Peertube + I am at least 16 years old and agree to the %1$s of this instance + Links + Change the color of links (URLs, mentions, tags, etc.) in messages + Reblogs header + Change the color of display name at the top of messages + Change the color of the user name at the top of messages + Change the color of the header for reblogs + Posts + Background color of posts in timelines + Reset colors + Tap here to reset all your custom colors + Reset + Icons + Color of bottom icons in timelines + Pin this tag + Logo of the instance + Edit profile + Make an action + Translation + Image preview + Text color + Change the text color in pots + Apply changes + You need to restart the application to apply changes + Restart + Use a custom theme + Allow to override colors of the selected theme above + Theming + Store before + The theme was exported + The theme has been successfully exported in CSV + Apply the primary color to the status bar + Status bar color + Restore a default theme + Import a theme + Tap here to import a theme from a previous export + Export the theme + Tap here to export the current theme + An error occurred when selecting the theme file + Theme Picker + Select a pre-installed theme + Themes + Apply the primary color to the navigation bar + Navigation bar color + The underlying color of the app’s content. + Background color + Accents select parts of the UI. + Accent color + Displayed most frequently across your app. + Primary color + Export bookmarks to the instance + Import bookmarks from the instance + User count + Status count + Instance count + Blocked + End in %s + What\'s new in %s + You can follow my account for updates + This instance is not available on https://instances.social + Display full link + Share link + The URL has been copied to the clipboard + Open with another app + Check redirect + This URL does not redirect + %1$s \n\nredirects to\n\n %2$s + Change the user agent + Set a custom user agent or leave blank + Allows to customize the user agent used for api calls or with the built-in browser. + Remove UTM parameters + The app will automatically remove UTM parameters from URLs before visiting a link. + Trends + Trending now + %d people talking + Twitter accounts (via Nitter) + Twitter usernames space separated + Identity proofs + Verified identity + Verified by %1$s (%2$s) + Delete the notification + Display more options + It is a Pixelfed story + Upload a media, it will be automatically added to your Pixelfed story. + Media successfully added to your story! + Action disabled + Unfollow + Something went wrong, please check your download directory in settings. + Announcements + No announcements! + Add a reaction + Use your favourite browser inside the app. Uncheck this feature to open links externally. + Video cache in MB, zero means no cache. + Watermarks + Automatically add a watermark at the bottom of pictures. The text can be customized for each account. + No distributors found! + You need a distributor for receiving push notifications.\nYou will find more details at %1$s.\n\nYou can also disable push notifications in settings for ignoring that message. + Select a distributor + diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml new file mode 100644 index 000000000..9c8f0b1d4 --- /dev/null +++ b/app/src/main/res/values-hu/strings.xml @@ -0,0 +1,1140 @@ + + + Menü megnyitása + Menü bezárása + Névjegy + A szerverről + Adatvédelem + Gyorsítótár + Kijelentkezés + Bejelentkezés + + Bezárás + Igen + Nem + Mégsem + Letöltés + %1$s letöltése + Média elmentve + Fájl: %1$s + Jelszó + E-mail + Fiókok + Tülkök + Cimkék + Mentés + Visszaállítás + Nincs találat! + Szerver + Szerver: mastodon.social + Aktív fiók: %1$s + Fiók hozzáadása + A toot tartalma a vágólapra lett másolva + A toot URL-je a vágólapra lett másolva + Módosítás + Kép kiválasztása… + Tisztítás + Felvétel + Összes törlése + Toot fordítása. + Időzítés + Szöveg és ikon méret + Szöveg méretének megváltoztatása: + Ikon méretének megváltoztatása: + Következő + Előző + Megnyitás a következővel: + Jóváhagyás + Média + Megosztás a következővel: + Megosztva a Fedilabon keresztül + Válaszok + Felhasználónév + Piszkozatok + Kedvencek + Új követők + Említések + Újratootok + Újratootok megjelenítése + Válaszok megjelenítése + Megnyitás böngészőben + Fordítás + Kérlek várj néhány másodpercet, mielőtt ezt tennéd. + + Kezdőlap + Helyi idővonal + Összevont idővonal + Beállítások + Kedvencek + Kommunikáció + Elnémított felhasználók + Letiltott felhasználók + Értesítések + Követési kérelmek + Beállítások + Fiók törlése + Valóban törölni szeretnéd a %1$s fiókot az alkalmazásból? + E-mail küldése + Változtatáshoz kattints az elérési útra + Sikertelen! + Időzített tootok + Az alábbi információ lehet, hogy nem mutat minden részletet a felhasználói profilból. + Emoji beszúrása + Az app nem gyűjtött egyéni emojikat. + Leküldéses értesítések + Biztos, hogy kijelentkezik? + Biztos, hogy kijelentkezteti: @%1$s@%2$s? + + Nincs megjeleníthető toot + Nincs megjeleníthető történet + Történetek + Ismétlés %1$s által + Hozzáadod ezt a tootot a kedvencekhez? + Eltávolítod ezt a tootot a kedvencek közül? + Megismétled ezt a tootot? + Megszünteted a toot ismétlését? + Rögzíted ezt a tootot? + Megszünteted a toot rögzítését? + Elnémítás + Letiltás + Bejelentés + Eltávolítás + Másolás + Megosztás + Megemlítés + Időzített elnémítás + Törlés & Újrafogalmazás + + Elnémítod ezt a fiókot? + Letiltod ezt a fiókot? + Jelented ezt a tootot? + Letiltod ezt a domént? + Megszünteted a fiók elnémítását? + Megszünteti a fiók letiltását? + + + Értesítés + Csendes + + + Eltávolítod ezt a tootot? + Törlöd és újrafogalmazod ezt a tootot? + + Könyvjelzők + Hozzáadás a könyvjelzőkhöz + Könyvjelző eltávolítása + Nincs megjeleníthető könyvjelző + Hozzáadtad a tootot a könyvjelzőkhöz! + Eltávolítottad a tootot a könyvjelzők közül! + + %d m + %d p + %d ó + %d n + + %d másodperc + %d másodperc + + + %d perc + %d perc + + + %d óra + %d óra + + + %d nap + %d nap + + + Figyelmeztetés + Mi jár a fejedben? + Toot! + QUEET! + cw + Írj egy tootot + Válaszolj egy tootra + Írj egy queetet + Válaszolj egy queetre! + Média kiválasztása + Hiba történt a média kiválasztásakor! + Törli ezt a médiát? + Ez a tootod üres! + A toot láthatósága + A tootok alapértelmezett láthatósága: + A tootot el lett küldve! + Erre a tootra válaszolsz: + Bizalmas tartalom? + + Posztolj a nyilvános idővonalakra + Ne posztolj nyilvános idővonalakra + Csak követőidnek posztolj + Csak megemlített felhasználóknak posztolj + + Nincs piszkozat! + Válassz egy tootot + Válassz egy fiókot + Válassz néhány fiókot + Eltávolítsuk a piszkozatot? + Klikkelj a gombra az eredeti toot megjelenítéséhez + Írd le a látássérültek számára + + Nincs elérhető leírás! + + Verzió: %1$s + Fejlesztő: + Licensz: + GNU GPL V3 + Forráskód: + Tootok fordítása: + Szerver keresése: + Ikontervező: + + Beszélgetés + + Nincs megjelenítető fiók + Nincs követési kérés + Tootok \n %1$s + Követettek \n %1$s + Követők \n %1$s + Rögzített \n %d + Engedélyez + Elutasít + + Nincs megjelenítendő időzített toot! + Írj egy tootot, és a felső menüből válaszd ki az Időzít elemet. + Törlöd az időzített tootot? + Média: %d + A tootot időzítésre került! + Az időzítés dátuma az aktuális óranak után kell lennie! + Be van kapcsolva az energiatakarékos üzemmód! Lehet, hogy nem a várt módon viselkedik. + + Az elnémítás minimális ideje egy perc. + %1$s el lett némítva %2$s időpontig.\n Az elnémítást a fiók profiloldalán lehet megszüntetni. + %1$s el lett némítva %2$s időpontig.\n Klikkelj ide a fiók némításának megszűntetéséhez. + + Nincs megjelenítendő értesítés + megemlített téged + új üzenetet írt + megismételte posztodat + lájkolta a posztodat + követett téged + megkérte, hogy követhesse + + és még egy értesítés + és még %d értesítés + + + %d lájkolás + %d lájkolás + + Törlöd az értesítést? + Törlöd az értesítéseket? + Az értesítést törölted! + Minden értesítést töröltél! + + Követettek + Követők + Rögzített + + Hiba a kliens id betöltésekor! + Nem lehet kapcsolódni a példány domainjéhez! + Nincs internetkapcsolat! + Letiltottad a fiókot! + A fiók letiltását megszűntetted! + A fiók el lett némítva! + A fiók nincs többé némítva! + A fiókot követted! + A fiókot már nem követed! + A toot meg lett ismételve! + A toot ismétlése meg lett szüntetve! + A toot hozzá lett adva a kedvencekhez! + A toot el lett távolítva a kedvencek közül! + Jelentve lett a toot! + Törölted a tootot! + Rögzítetted a tootot! + A toot rögzítését megszüntetted! + Ááá! Hiba történt! + Hiba történt! A szerver nem adott vissza engedélyezési kódot! + A szerver doménje érvénytelennek tűnik! + Hiba történt a fiókváltás során! + Hiba történt a keresés közben! + A profiladatokat elmentettük! + Nem lehet végrehajtani ezt a műveletet + Média elmentve! + Hiba történt fordítás közben! + Ki van kapcsolva a fordítás a beállításokban + Piszkozat elmentve! + Biztos vagy benne, hogy ez a szerver megenged ennyi karaktert? Ez az érték általában 500 karakter körül van. + A tootok láthatósága megváltozott a %1$s fiók számára + + Tootok száma letöltésenként + Mindig + WiFi + Kérdezz rá + Média betöltése + Képek betöltése + Mutass többet… + Mutass kevesebbet… + Bizalmas tartalom + GIF avatarok kikapcsolása + Útvonal: + Piszkozatok automatikus mentése + Média URL-jének hozzáadása a tootokhoz + Értesítsen, amikor valaki követ + Értesíts, amikor valaki megismétli a posztodat + Értesíts, ha valaki lájkolja a posztodat + Értesíts, amikor valaki megemlít téged + Értesíts, amikor egy szavazás végetér + Értesítés az új bejegyzésekről + Kérdezz vissza újratootolás előtt + Megerősítés kérése kedvencekhez hozzáadás előtt + Értesítés csak WiFi használatakor + Kérsz értesítést? + Csendes értesítések + NSFW tartalom megjelenítésének ideje (mp, 0 érték kikapcsolja) + Médialeírás időtúllépése (mp, 0 érték kikapcsolja) + Profil szerkesztése + Egyéni megosztás + Az egyéni megosztási URL… + Rövid bemutatkozás… + Fiók zárolása + Változások mentése + Válassz egy képet a fejléchez + Előnézeti képek méretre szabása + Automatikusan darabolja fel az 500 karakternél hosszabb tootokat a válaszokban + Elérted az 160 karakteres limitet! + Elérted az 30 karakteres limitet! + Kezdet: + Befejezés: + Az időnek hosszabbnak kell lennie, mint %1$s + Az időnek rövidebbnek kell lennie, mint %1$s + Kezdés időpontja + Befejezés időpontja + Beépített böngésző használata + Egyéni lapok + Javascript engedélyezése + cw automatikus kibontása + Harmadik féltől származó sütik engedélyezése + A te API-kulcsod (Yandex esetén üresen hagyhatja) + + Sötét + Világos + Fekete + + LED színének beállítása: + + Kék + Világoskék + Bíborvörös + Zöld + Piros + Sárga + Fehér + + Követés + Letiltás feloldása + Elnémítás + Némítás feloldása + Kérés elküldve + Követ téged + Keresés + Első betű nagybetűsítése a válaszokban + Képek átméretezése + Videók átméretezése + + Push-értesítések + Kérjük erősítsd meg, mely push-értesítéseket szeretnéd megkapni. + Ezeket az értesítéseket később ki- vagy bekapcsolhatod (Értesítések lapon). + + + Gyorsítótár ürítése + A gyorsítótárban %1$s megjelenítendő elem van.\n\nSzeretnéd törölni őket? + Mb + Gyorsítótár ki lett űrítve! %1$s lett felszabadítva + + Cím + Cím… + Leírás + Kulcsszavak + Kulcsszavak… + + Szinkronizálás + Szűrés + Tootjaid + Saját értesítések + Nyilvános + Nem megjelenített + Privát + Közvetlen + Néhány kulcsszó… + Média megjelenítése + Rögzített tootok megjelenítése + Nincs találat! + %1$s fiók tootjainak archiválása + %1$s új tootjait importáltuk + %1$s új értesítés lett importálva + + Dátum alapján csökkenő + Dátum alapján növekvő + + + Nem + Csak + Mindkettő + + Nem találtunk tootokat az adatbázisban. Kérjük, letöltésükhöz használd a szinkronizálás gombot. + + Rögzített adatok + A készüléken csak a fiókok alapadatait tároljuk. + Ezek az adatok szigorúan bizalmasak és csak az alkalmazás fér hozzá. + Az alkalmazás eltávolítása azonnal törli ezeket az adatokat.\n + ⚠ Felhasználónevet és jelszavakat sohasem tárolunk. Csak a szerverre való biztonságos (SSL) bejentkezés során használjuk. + + Engedélyek: + - ACCESS_NETWORK_STATE: Arra használjuk, hogy megállapítsuk, a kliens WiFi-hálózathoz kapcsolódik-e.\n + - INTERNET: Szerverek lekérdezésekor használatos.\n + - WRITE_EXTERNAL_STORAGE: Média vagy magának alkalmazásnak az SD-kártyára áthelyezéséhez használatos.\n + - READ_EXTERNAL_STORAGE: Médiát ad hozzá toothoz.\n + - BOOT_COMPLETED: Az értesítési szolgáltatás indítására használatos.\n + - WAKE_LOCK: Az értesítési szolgáltatás használja működés közben. + + API jogosultságok: + - Olvas: Adat olvasása.\n + - Írás: Posztok létrehozása és média csatolása posztokhoz..\n + - Követ: Követés, követés megszűntetése, letiltás és tiltás feloldása. \n\n + ⚠ Ezek az akciók csak akkor hajtódnak végre, amikor a felhasználó kéri. + + Követés és könyvtárak + Az alkalmazásnem használ követési eszközöket (hallgatóság mérése, hibajelentés, stb.) és nem tartalmaz hirdetéseket.\n\n + A könyvtárak használata minimalizált: \n + - Glide: Médiák kezelése\n + - Android-Job: Szolgáltatások kezelése\n + - PhotoView: Képek kezelése\n + + Tootok fordítása + Az alkalmazás lehetővé teszi a tootok lefordítását az eszköz nyelvi beállításainak megfelelően, a Yandex API segítségével.\n +A Yandexnek megvan a saját adatvédelmi szabályzata, ami itt található: https://yandex.ru/legal/confidential/?lang=en + Köszönet a következőknek: + Szűrés reguláris kifejezések használatával + Keresés + Törlés + További tootok betöltése… + + Listák + Biztos benne, hogy véglegesen törölni szeretné ezt a listát? + Ez a lista még üres. Mikor a lista tagjai posztolnak, azok itt fognak megjelenni. + Hozzáadás a listához + Új lista + Lista törlése + Lista szerkesztése + Új listacím + A fiók hozzá lett adva a listához! + Még nincs egyetlen listája sem! + + %1$s elköltözött ide: %2$s + Nem sikeres a hitelesítés? + Íme néhány hasznos tipp:\n\n + - Ellenőrizd, hogy nem gépelted-e el a szerver nevét.\n\n + - Ellenőrizd, hogy a szervered elérhető\n\n + - Használd az alsó linket, ha két faktoros hitelesítést (2FA) használsz (miután kitöltötted a szerver nevét)\n\n + - Ezt a linket 2FA nélkül is használhatod\n\n + - Ha még mindig nem sikerül bejelentkezni, jelentsd a Framagiton a https://framagit.org/tom79/fedilab/issues címen + + Betöltöttük a médiát. Klikkelj ide a megjelenítéshez. + Ez a folyamat sokáig tarthat. Értesítünk majd, amikor befejeződött. + Még nem fejeztük be, kérlek várj még… + Exportáld a posztokat + Exportáld %1$s posztjait + %1$s tootot exportáltunk a %2$s tootból. + Valamilyen hiba történt %1$s adatainak exportálása közben + Valamilyen hiba történt az exportálás közben! + Valamilyen hiba történt az importálás közben! + + Proxy + Proxy bekapcsolása + Proxyszerver neve + Port + Felhasználónév + Jelszó + Toot részleteinek hozzaádása megosztáskor + Támogasd az appot a Liberapayen + A reguláris kifejezés hibát tartalmaz! + Ezen a szerveren nem találtunk idővonalat! + Töröljuk ezt a szervert? + Fordítsd erre: + Szerver követése + Már követed ezt a szervert! + Mostantól követed a szervert! + Együttműködők + Információ + %s újratootjainak elrejtése + Mutassa a profilban + %s újratootjainak megjelenítése + Ne mutassa a profilban + A fiókot mostantól mutatjuk a profilban + A fiókot többé nem mutatjuk a profilban + Mostantól láthatóak az újratootok! + Mostantól el vannak rejtve az újratootok! + Közvetlen üzenet + Szűrők + Nincs megjeleníthető szűrő. Kattintson a \"+\" gombra a létrehozáshoz. + Kulcsszó vagy kifejezés + Kezdőlap idővonala + Nyilvános idővonalak + Értesítések + Beszélgetések + A szöveg kis- vagy nagybetűs írásától és a toot tartalmi figyelmeztetésétől függetlenül találatnak minősül + Eltávolít elrejtés helyett + A kiszűrt tootok visszafordíthatatlanul eltűnnek, még ha később el is távolítod a szűrőt + Amennyiben a kulcsszó vagy kifejezés kizárólagosan alfanumerikus, csak akkor minősül találatnak, ha a teljes kulcsszót/kifejezést megtalálja + Teljes szó + Szűrés kontextusai + Egy vagy több kontextus, amire a szűrő alkalmazandó + Lejárat dátuma + Törlöd a szűrőt? + Szűrő módosítása + Szűrő létrehozása + Kit érdemes követni + Nincs több listázott fiók jelenleg! + Követés + Összes kiválasztása + Összes kijelölés megszüntetése + %s követése elkezdődött! + %s lista létrehozása folyamatban + Hozzáadjuk a fiókot a listához + Hozzáadtuk a fiókokat a listához + Hozzáadjuk a fiókokat a listához + Nem hoztál még létre listát. Kattintson a \"+\" gomb-ra egy új hozzáadásához. + Kit érdemes követni + Trunk API + A fiók(ok) nem követhetőek + Betöltjük a távoli fiók adatait + Rejtett médiák automatikus kibontása + Új követő + Új újratoot + Új kedvenc + Új említés + A szavazás véget ért + Új toot + Tootok elmentése + Új bejegyzések + Média letöltése + Értesítési hang megváltoztatása + Hang kiválasztása + Idősáv beállítása + HOGYAN videók + Betöltjük a távoli beszélgetést! + Nincs letiltott domén! + Domén letiltásának megszűntetése + Biztosan megszűnteted %s letiltását? + Biztosan letiltod %s domént?\n\nNem fogsz tőle semmilyen tartalmat látni, semmilyen nyilvános idővonalon vagy az értesítéseid között. A doménből származó követőidet eltávolítjuk. + Letiltott domének + Domént letilt + Domén le van tiltva + A tartomány nincs többé letiltva! + Betöltjük a távoli posztot + Hozzászólás + Peertube szerver + Legyél te az első, aki hozzászól ehhez a videóhoz a jobb felső gomb segítségével! + %s megtekintés + Időtartam: %s + Szerver hozzáadása + A hozzászólás nem engedélyezett ennél a videónál! + Válassz egy felbontást + Peertube kedvencek + Hozzáadtuk a videót a könyvjelzőkhöz! + Eltávolítottuk a videót a könyvjelzők közül! + Nincs Peertube videó a kedvenceid között! + Csatorna + Videók + Csatornák + Emoji One használata + Információ + Előnézet megjelenítése minden toothoz + Új UX/UI tervező + Videó előnézetek megjelenítése + A fiók id-jét átmásoltuk a vágólapra! + Nyelv megváltoztatása + Alapértelmezett nyelv + Túl hosszú tootok csonkolása + Csonkold a tootot, ha hosszabb \'x\' sornál. Nullánál nem csonkol. + Mutass többet + Mutass kevesebbet + Címkék kezelése + Ez a címke már létezik! + Eltároltuk a címkét! + Megváltoztattuk a címkét! + Töröltük a címkét! + Újratoot időzítése + Az újratoot időzítésre került! + Nincs megjeleníthető időzített újratoot! + Újratoot időzítése opciót.]]> + Művészies idővonal + Menü megnyitása + Visszalépés + Alkalmazás logója + Profilkép + Profilbanner + Lépj kapcsolatba a szerver adminjával + Új hozzáadása + MastoHost logó + Emoji-választó + Frissítés + Beszélgetés kibontása + Fiók eltávolítása + Letiltott domén törlése + Egyéni emoji-választó + Videó lejátszása + Új toot + Hivatkozás előnézete + Média elrejtése + Könyvjelzőikon + Média-leírás hozzáadása (látássérültek hasznára) + + Soha + 30 perc + 1 óra + 6 óra + 12 óra + 1 nap + 1 hét + + Ebbe a mezőbe, írd a szervered host-nevét.\nPéldául, ha a fiókodat a https://mastodon.social\n címen nyitottad, akkor írd, hogy mastodon.social (https:// nélkül)\n + Kezdd beírni az első néhány betűt és a többit segítünk kiegészíteni.\n\n + ⚠ A Bejelentkezés gomb csak akkor fog működni, ha a szervernév érvényes és a szerver elérhető! + + További információ + + Nyelvek + Csak média + NSFW tartalom megjelenítése + Crowdin fordítások + Crowdin adminisztrátor + Az alkalmazás fordítása + A Crowdinról + Bot + Pixelfed szerver + Masztodon szerver + Bármelyik + Mindegyik + Egyik sem + Bármelyik a következő szavakból (szóközzel elválasztva) + Minden szó (szóközzel elválasztva) + Néhány szűrő szó hozzáadása (szóközzel elválasztva) + Oszlop nevének megváltoztatása + Misskey szerver + Eszközödön nincs olyan alkalmazás, ami ezt a linket megnyitná. + Előfizetések + Áttekintés + Népszerű + Nemrég hozzáadott + Helyi + Feltöltés + Válasz + Hozzászólás törlése + Biztos, hogy törli ezt a hozzászólást? + Teljes képernyős videó + Videó-módok + Feltöltendő fájl kiválasztása + Videóim + Cím + Licensz + Kategória + Nyelv + Ez a videó felnőtt vagy obszcén tartalmú + Hozzászólások engedélyezése + Videó frissítése + Leírás + A videót frissítettük! + Feltöltés megszakítva! + A videót feltöltése bejefeződött! + Feltöltés folyamatban, kérjük várj… + Klikkelj ide a videó adatok szerkesztéséhez. + Videó törlése + Biztos törölni akarod a videót? + NSFW videók megjelenítése + Nincs megjeleníthető videó! + Hozzászólok + Megosztás + Ütemezési mód választása + Erről az eszközről + Szerverről + Tootok (szerver) + Tootok (eszköz) + Módosítás + Új tootok megjelenítése a \"Továbbiak betöltése\" gomb fölött + Idővonalak + Interfész + Kapcsolatok + %1$s hozzászólt a %2$s videódhoz]]> + %1$s követi a %2$s csatornádat]]> + %1$s követi a fiókodat]]> + %1$s videódat közzétettük]]> + %1$s videód importálása sikerült]]> + %1$s videód importálása nem sikerült]]> + %1$s közzétett egy új videót: %2$s]]> + %1$s videód tiltólistára került]]> + %1$s videód lekerült a tiltólistáról]]> + Adat exportálása + Adat importálása + Importálandó fájl kiválasztása + Hiba történt a mentés-fájl létrehozásakor! + Nyilvános hozzászólás hozzáadása + Hozzászólás küldése + Nincs internet-kapcsolat. Üzenetedet a piszkozatok között tároltuk. + Sima szöveg + HTML + Markdown + Kijelentkezés fiókból + Összes + Az app támogatása + Az Open Collective segít egy új közösség gyors létrehozásában, a pénzgyűjtésben és annak transzparens kezelésében. + Hivatkozás másolása + Csatlakozás + Normális + Kompakt + Konzol + Megjelenítési mód beállítása + Biztonsági szolgáltató frissítése + Követési domének frissítése + A követési adatbázist frissítettük! + Az alkalmazás blokkolta a http hívásokat + Letiltott hívások listája + Küldés + Exportáltuk az adatbázist! + Népszerű hashtagek + Idővonal szűrése címkékkel + Nincs címke + Rejtsd el az értesítés gombot az értesítés lapon + Kép csatolása URL megosztásakor + + Szavazás + Szavazások + Szavazás létrehozása + 1. lehetőség + 2. lehetőség + %d. lehetőség + Legalább két lehetőséget kell megadni egy szavazás létrehozásához! + Kész + %s-kor ér véget + Szavazás frissítése + Szavazás + Véget ért egy szavazás, amiben részt vettél + Véget ért egy szavazás, amiben részt vettél + Testreszabás + Kategóriák + Idősáv + Speciális + \"Új\" szimbólumm megjelenítése olvasatlan tootok mellett + Peertube + Idővonal áthelyezése + Idővonal elrejtése + Idővonalak átrendezése + Véglegesen törölted a listát + A követett szerver el lett távolítva + A rögzített címke el lett távolítva + Visszavonás + Legalább két látható lapnak kell maradni! + Idővonalak átrendezése + A fő idővonalakat csak elrejteni lehet! + BBCode + Minden médiát jelölj meg érzékenynek + GNU szerver + Gyorsítótárazott üzenet + Címkék továbbítasa a válaszokban + Nyomd meg hosszan a média tárolásához + Érzékeny média homályosítása + Idővonalak megjelenítése listában + Idővonalak megjelenítése + Bot-fiókok megjelölése a tootokban + Címkék kezelése + Pozíció megjegyzése a kezdőlap idővonalán + Előzmények + Lejátszási listák + Megjelenítendő név + Nincs még lejátszási listád. Klikkelj a \'+\' jelre új lejátszási lista hozzáadásához + Meg kell adnod egy megjelenítési nevet! + Csatorna kiválasztása szükséges, ha a lejátszási lista nyilvános. + Lejátszási lista létrehozása + Ez a lejátszási list még üres. + Ismétlés + Galéria + Emoji + Matrica + Radír + Szöveg + Szűrő + Ecset + Biztos, hogy a kép mentése nélkül kilép? + Elvet + Mentés folyamatban… + A kép sikeresen elmentve! + Nem sikerült elmenteni a képet + Áttetszőség + Fényképszerkesztő bekapcsolása + Elem hozzáadása + Elem eltávolítása + Beszélgetés némítása + Beszélgetés némításának visszavonása + A beszélgetés már nincs némítva! + A beszélgetés némítva van + Alkalmazásfunkciók megnyitása + Időzített elnémítás + Fiók megemlítése + Gyorsítótár frissítése + Üzenet megemlítese + Hírek + Általános + Regionális + Művészet + Újságírás + Aktivizmus + Játékok + Technológia + Felnőtt tartalom + Furry + Étel + A példány logója + Valami rosszul sikerült az elérhető példányok keresésekor! + Csatlakozás a Mastodonhoz + Válasszon egy példányt egy kategória választásával, majd kattintson egy pipa gombra. + Válasszon egy példányt egy pipa gombra kattintással. + %1$s felhasználó + Jelszó megerősítése + Elfogadom a %1$s és a %2$s + kiszolgáló szabályait + szolgáltatás feltételeit + Regisztráció + A példány meghívásos alapon működik. A fiókját kézzel kell elfogadnia egy rendszergazdának, mielőtt az használhatóvá válna. + Töltse ki az összes mezőt! + A jelszavak nem egyeznek! + Az e-mail-cím nem tűnik érvényesnek! + A felhasználóneve egyedi lesz ezen a kiszolgálón: %1$s + Egy megerősítő e-mailt fog kapni + Használjon legalább 8 karaktert + A jelszónak legalább 8 karaktert kellene tartalmaznia + A felhasználónévnek csak betűket, számokat és aláhúzásokat kellene tartalmaznia + Fiók létrehozva! + Your account has been created!\n\n + Think to validate your email within the 48 next hours.\n\n + You can now connect your account by writing %1$s in the first field and click on Connect.\n\n + Important: If your instance required validation, you will receive an email once it is validated! + + Menti az üzenetet piszkozatként? + Adminisztráció + Jelentések + Nincs megjelenítendő jelentés! + Reconnect the account + The application failed to access the admin features. You might need to reconnect the account for having the correct scope. + Unresolved + Remote + Active + Pending + Disabled + Silenced + Suspended + Permissions + Email status + Login status + Joined + Most recent IP + Warn + Disable + Silence + Notify the user per e-mail + Custom warning + User + Moderator + Administrator + Confirmed + Not confirmed + Reported statuses + Account + Undo silence + Undo disable + Suspend + Undo suspend + The account is silenced! + The account is no longer silenced! + The account is suspended! + The account is no longer suspended! + The account is disabled! + The account is no longer disabled! + The account has been warned! + Display the admin menu + Display the admin feature in statuses + Allow + The account is approved! + The account is rejected! + Assign to me + Unassign + Mark as resolved + Mark as unresolved + Empty content! + Display Fedilab features button + The application needs to access audio recording + Voice message + Gyors válasz bekapcsolása + A fiók, melynek válaszol, lehet hogy nem fogja látni az üzenetét! + Kikapcsolt állapotban az alkalmazás mindig a legfrissebb bejegyzéseket tölti be + Kikapcsolt állapotban a bizalmas médiák egy gombbal lesznek elrejtve + Médiák mentése teljes méretben az előnézetek hosszú érintésekor + Három pont gomb hozzáadása a címkék/példányok/listák felsorolásának jobb felső sarkában + Az idősáv alatt fog az alkalmazás értesítéseket küldeni. A fordítottját is beállíthatja (tehát némíthat) a jobb oldali legördülővel. + Fedilab gomb megjelenítése a profilkép alatt. Ez egy rövidítés az alkalmazáson belüli funkciókhoz. + Közvetlen válasz engedélyezése a bejegyzések alatt + Previews will not be cropped in timelines + Allow to play embedded videos directly in timelines + Allow to reverse the way to read statuses that are displayed once tapping the fetch more button + This option allows to support recent cipher suites. It is useful for older Android devices or if you cannot connect to your instance. + Exclusively for Peertube videos. Switch this mode if you cannot play them. + These tags will allow to filter statuses from profiles. You will have to use the context menu for seeing them. + Automatically insert a line break after the mention to capitalize the first letter + Allow content creators to share statuses to their RSS feeds + Compose + Maximum retry times when uploading media + Create a new Folder here + Enter the folder name + Please enter a valid folder name + This folder already exists.\n Please provide another name for the folder + Select + Default Directory + Folder + Create folder + Display a toast message after an action has been completed (boost, fav, etc.)? + Muted instances have been exported! + Add an instance + Export instances + Import instances + Crash reports + Enable crash reports + If enabled, a crash report will be created locally and then you will be able to share it. + Fedilab leállt :( + Küldhetsz nekem e-mailt a hibajelentéssel. Ez segít majd a javításnál :)\n\nHozzáadhatsz még további tartalmakat is. Köszönöm! + Use the wysiwyg + When enabled, you will be able to format your text easily with tools. + Statistics + Total statuses + Number of boosts + Number of favourites + Number of mentions + Number of follows + Number of polls + Number of replies + Number of statuses + Statuses + Visibility + Number with media + Number with sensitive media + Number with CW + First status date + Last status date + First notification date + Last notification date + Frequency + %s statuses per day + %s notifications per day + Date range + Groups + No groups! + Disable custom animated emojis + Charts + Display charts + The application collects your local data, please wait… + Backup + Auto backup statuses + This option is per account. It will launch a service that will automatically store your statuses locally in the database. That allows to get statistics and charts + Auto backup notifications + This option is per account. It will launch a service that will automatically store your notifications locally in the database. That allows to get statistics and charts + Report account + Send an invitation + Your instance does not allow to register a new account! + + %d szavazat + %d szavazat + + + %d voter + %d voters + + + Egyetlen választás + Több választás + + + 5 perc + 30 perc + 1 óra + 6 óra + 1 nap + 3 nap + 7 nap + + + Megjelenítés böngészőben + Élő közvetítés + + For joining my instance \"%1$s\", you can download Fedilab:\n\nF-Droid: %2$s\nGoogle: %3$s\n\nThen open the link below with Fedilab and create your account :)\n\n%4$s + + Your poll can\'t have duplicated options! + For all accounts + Database cache + Clear your home timeline cache + Clear your cached statuses + Clear your bookmarks + Files in cache + Total notifications + Hide menu items + Fedilab is running live notifications + For %1$s accounts with %2$s events + Live notifications for %1$s + Live notifications will be enabled for this account. + Clear cache when leaving + The cache (media, cached messages, data from the built-in browser) will be automatically cleared when leaving the application. + Do you want to unfollow this account? + Show confirmation dialog before unfollowing + Replace Youtube with Invidio.us + Invidious is an alternative front-end to YouTube + Enter your custom host or leave blank for using invidious.snopyta.org + Replace Twitter with Nitter + Nitter is an open source alternative Twitter front-end focused on privacy. + Enter your custom host or leave blank for using nitter.net + Replace Instagram with Bibliogram + Bibliogram is an open source alternative Instagram front-end focused on privacy. + Enter your custom host or leave blank for using bibliogram.art + Replace Reddit with Libreddit + Libreddit is an open source alternative Reddit front-end focused on privacy. + Enter your custom host or leave blank for using libredd.it + Replace Medium links + Replace medium.com links with an open source alternative front-end focused on privacy. + Default: scribe.rip + Replace Wikipedia links + Replace Wikipedia link with an open source alternative front-end focused on privacy. + Default: wikiless.org + Hide Fedilab notification bar + For hiding the remaining notification in the status bar, tap on the eye icon button then uncheck: \"Display in status bar\" + Use a push notifications system for getting notifications in real time. + No live notifications + Live notifications + Notifications will be fetched every 15 minutes. + Add notes + Notes for the account + Allow to compress large photos into smaller sized photos with very less or negligible loss in quality of the image. + Allow compressing videos while maintaining their quality. + The app is compressing the media, it can be quite long… + Change app icon + Tap to change the app icon + Post + Visibility of the post + Tap here to add photos + Accepted Formats: jpeg, png, gif \n\nMax File Size: 15 MB \n\nAlbums can contain up to 4 photos or videos + Upload media + Add an optional caption + The app received a very long error message from the API %1$s + Message preview + Add mentions in each message + Fetching conversation + Order by + Title for the video + Join Peertube + I am at least 16 years old and agree to the %1$s of this instance + Links + Change the color of links (URLs, mentions, tags, etc.) in messages + Reblogs header + Change the color of display name at the top of messages + Change the color of the user name at the top of messages + Change the color of the header for reblogs + Posts + Background color of posts in timelines + Reset colors + Tap here to reset all your custom colors + Reset + Icons + Color of bottom icons in timelines + Pin this tag + Logo of the instance + Edit profile + Make an action + Translation + Image preview + Text color + Change the text color in pots + Apply changes + You need to restart the application to apply changes + Restart + Use a custom theme + Allow to override colors of the selected theme above + Theming + Store before + The theme was exported + The theme has been successfully exported in CSV + Apply the primary color to the status bar + Status bar color + Restore a default theme + Import a theme + Tap here to import a theme from a previous export + Export the theme + Tap here to export the current theme + An error occurred when selecting the theme file + Theme Picker + Select a pre-installed theme + Themes + Apply the primary color to the navigation bar + Navigation bar color + The underlying color of the app’s content. + Background color + Accents select parts of the UI. + Accent color + Displayed most frequently across your app. + Primary color + Export bookmarks to the instance + Import bookmarks from the instance + User count + Status count + Instance count + Blocked + End in %s + What\'s new in %s + You can follow my account for updates + This instance is not available on https://instances.social + Display full link + Share link + The URL has been copied to the clipboard + Open with another app + Check redirect + This URL does not redirect + %1$s \n\nredirects to\n\n %2$s + Change the user agent + Set a custom user agent or leave blank + Allows to customize the user agent used for api calls or with the built-in browser. + Remove UTM parameters + The app will automatically remove UTM parameters from URLs before visiting a link. + Trends + Trending now + %d people talking + Twitter accounts (via Nitter) + Twitter usernames space separated + Identity proofs + Verified identity + Verified by %1$s (%2$s) + Delete the notification + Display more options + It is a Pixelfed story + Upload a media, it will be automatically added to your Pixelfed story. + Media successfully added to your story! + Action disabled + Unfollow + Something went wrong, please check your download directory in settings. + Announcements + No announcements! + Add a reaction + Use your favourite browser inside the app. Uncheck this feature to open links externally. + Video cache in MB, zero means no cache. + Watermarks + Automatically add a watermark at the bottom of pictures. The text can be customized for each account. + No distributors found! + You need a distributor for receiving push notifications.\nYou will find more details at %1$s.\n\nYou can also disable push notifications in settings for ignoring that message. + Select a distributor + diff --git a/app/src/main/res/values-hy/strings.xml b/app/src/main/res/values-hy/strings.xml new file mode 100644 index 000000000..d4f6fb901 --- /dev/null +++ b/app/src/main/res/values-hy/strings.xml @@ -0,0 +1,1141 @@ + + + Բացել ընտրացանկը + Փակել ընտրացանկը + Տեղեկություններ + Հանգույցի մասին + Գաղտնիություն + Հիշապահեստ + Ելք + Մուտք + + Փակել + Այո + Ոչ + Չեղարկել + Ներբեռնել + Ներբեռնել %1$s-ը + Մեդիան պահված է + Նիշք․ %1$s + Գաղտնաբառ + Էլ․հասցե + Հաշիվներ + Թթեր + Պիտակներ + Պահել + Վերականգնել + Ոչ մի արդյունք + Հանգույց + Հանգույց․ mastodon.social + Օգտագործվող հաշիվ՝ %1$s + Ավելացնել հաշիվ + Թութի բովանդակությունը պատճենվել է սեղմատախտակին + The URL of the toot has been copied to the clipboard + Փոխել + Ընտրել նկար… + Մաքրել + Խցիկ + Ջնջել ամենը + Թարգմանել այս թութը։ + Հերթ + Տեքստի և պատկերակների չափսերը + Փոխել տեքստի ներկայիս չափսը․ + Փոխել պատկերակի ներկայիս չափսը․ + Հաջորդը + Նախորդը + Բացել այլ հավելվածով + Վավերացնել + Մեդիա + Կիսվել + Տարածվել է Մաստալաբի միջոցով + Արձագանքներ + Օգտանուն + Սևագրեր + Հավանածներ + Նոր հետևողներ + Հիշատակումներ + Տարածածներ + Ցուցադրել տարածածները + Ցուցադրել արձագանքները + Բացել զննիչում + Թարգմանել + Սպասիր մի քանի վայրկյան մինչև այս գործողությունը կատարելը։ + + Հիմնական + Տեղական հոսք + Դաշնային հոսք + Ընտրանքներ + Հավանածներ + Հաղորդակցում + Խլացված օգտատերեր + Արգելափակված օգտատերեր + Ծանուցումներ + Հետևելու հայտեր + Կարգավորումներ + Ջնջել հաշիվ + Ջնջե՞լ %1$s հաշիվը հավելվածից + Ուղարկել էլփոստ + Կտտացրու հետագծի վրա՝ այն փոխելու համար + Ձախողում + Հերթագրված թութեր + Սույն տեղեկատվությունը կարող է օգտատիրոջ էջն արտացոլել ոչ լիարժեք։ + Էմոջի ներմուծել + Ներկա պահին հավելվածը չունի հավաքագրած էմոջիներ։ + Push notifications + Are you sure you want to logout? + Are you sure you want to logout @%1$s@%2$s? + + Թութ չկա + No stories to display + Stories + Խրախուսվել է %1$s-ի կողմից + Ավելացնե՞լ այս թութը նախընտրություններին + Հեռացնե՞լ այս թութը Նախընտրություններից + Խրախուսե՞լ այս թութը։ + Ապախրախուսե՞լ այս թութը։ + Մեխե՞լ այս թութը։ + Արձակե՞լ այս թութը։ + Խլացնել + Արգելափակել + Ահազանգել + Հեռացնել + Պատճենել + Տարածել + Հիշատակել + Ժամանակավոր խլացում + Delete & re-draft + + Խլացնե՞լ այս հաշիվը + Արգելափակե՞լ այս հաշիվը + Ահազանգե՞լ այս հաշվի մասին + Block this domain? + Unmute this account? + Unblock this account? + + + Notify + Silent + + + Հեռացնե՞լ այս թութը + Delete & re-draft this toot? + + Bookmarks + Add to bookmarks + Remove bookmark + No bookmarks to display + Status has been added to bookmarks! + Status was removed from bookmarks! + + %d վրկ + %d ր + %d ժ + %d օր + + %d second + %d seconds + + + %d minute + %d minutes + + + %d hour + %d hours + + + %d day + %d days + + + Զգուշացում + Մտքիդ ի՞նչ կա + ԹՈՒԹԵԼ! + QUEET! + cw + Թութ գրել + Արձագանքել թութին + Write a queet + Reply to a queet + Ընտրել մեդիա + Մեդիան ընտրելու ընթացքում սխալ է հայտնվել + Ջնջե՞լ այս մեդիան + Քո թութը դատարկ է + Թութի տեսանելիությունը + Թութերի սկզբնադիր տեսանելիությունը՝ + Թութն ուղարկված է + Արձագանքում ես տվյալ թութին․ + Զգայուն բովանդակությու՞ն + + Տեղադրել հանրային հոսքերին + Չտեղադրել հանրային հոսքերին + Գրառել միայն հետևորդների համար + Գրառել միայն հիշատակված օգտատերերի համար + + Ոչ մի սևագիր + Ընտրել թութ + Ընտրել հաշիվ + Ընտրել մի քանի հաշիվ + Հեռացնե՞լ սևագիրը + Կտտացրեւ կոճակին՝ բնօրինակ թութը տեսնելու համար + Նկարագրիր՝ տեսողության խնդիրներ ունեցողների համար + + Նկարագրություն չկա + + Թողարկում %1$s + Մշակող․ + Արտոնագիր․ + GNU GPL V3 + Սկզբնական կոդ․ + Թութերի թարգմանությունը․ + Փնտրել հանգույցներ․ + Պատկերակի հեղինակ․ + + Զրույց + + Ցուցադրելու հաշիվ չկա + Հետևելու հայտեր չկան + Թութեր \n %1$s + Հետևում եմ \n %1$s + Հետևում են \n %1$s + Մեխեր \n %d + Թույլատրել + Մերժել + + Հերթագրված թութեր չկան + Թութ գրիր, հետո ընտրիր Հերթը գլխավոր մենյուից + Ջնջե՞լ հերթագրված թութը + Մեդիա %d + Թութը հերթագրված է + Հերթագրման ամսաթիվը պետք է լինի ավելի ուշ քան ընթացիկ ժամը + Մարտկոցի տնտեսումը միացված է։ Հնարավոր է չաշխատի այնպես ինչպես ակնկալում եք + + Խլացնելու ժամանակահատվածը պետք է լինի 1 րոպեից ավել։ + %1$s-ը խլացվել է մինչև %2$s։ \n Կարող ես ապախլացնել հաշիվն իր էջից։ + %1$s-ը խլացվել է մինչև %2$s։ \n Կտտացրու այստեղ՝ ապխլացնելու համար։ + + Ծանուցում չկա + -ը հիշատակել է քեզ + wrote a new message + -ը խրախուսել է քո թութը + -ը գրառումդ ավելացրել է նախընտրումներին + -ը հետևում է քեզ + asked to follow you + + և մեկ այլ ծանուցում + և %d այլ ծանուցումներ + + + %d like + %d likes + + Ջնջե՞լ ծանուցումը + Ջնջե՞լ բոլոր ծանուցումները + Ծանուցումը ջնջված է + Բոլոր ծանուցումները ջնջված են + + Հետևում եմ + Հետևորդներ + Մեխած + + Հաճախորդի id-ն հնարավոր չէ ստանալ + Unable to connect to instance domain! + Կապ չկա + Հաշիվն արգելափակված է + Հաշիվն այլևս արգելափակված չէ + Հաշիվը խլացված է + Հաշիվն այլևս խլացված չէ + Հետևում ես հաշվին + Այլևս չես հետևում հաշվին + Թութը խրախուսվել է + Թութն այլևս չի խրախուսվում + Թութն ավելացվել է քո նախընտրություններին + Թութը հեռացվել է նախընտրություններից + Թութի վերաբերյալ ահազանգ կա + Թութը ջնջվել է + Թութը մեխվել է + Թութն ապամեխվել է + Ուպս! Ինչ-որ բան այն չէ + Ինչ որ բան այն չէ! Հանգույցը չվերադարձրեց հաստատման կոդը + Հանգույցի դոմեյնը կարծես անվավեր է + Ինչ-որ սխալ տեղի ունեցավ հաշիվը փոխելու ընթացքում + Փնտրելիս սխալ տեղի ունեցավ + Էջի բովանդակությունը պահպանված է + Անհնար է ինչ-րո բան անել + Մեդիան պահպանված է + Թարգմանության ընթացքում սխալ տեղի ունեցավ + Translations are disabled in settings + Սևագիրը պահված է + Վստա՞հ ես որ սույն հանգույցը թույլատրում է նիշերի նման քանակ։ Սովորաբար առավելագույնը 500 նիշն է։ + Թութերի տեսանելությունը փոխվել է %1$s հաշվի համար + + Թութերի քանակը մեկ բեռնման համար + Միշտ + WIFI + Հարցնել + Բեռնել մեդիան + Բեռնել նկարները + Ցույց տալ… + Ցույց չտալ… + Զգայուն բովանդակություն + Անջատել գիֆ նկարները + Հետագիծ․ + Ինքնաշխատ պահել սևագրերը + Թութում ներառել մեդիայի հղումը + Տեղեկացնել, երբ որևէ մեկը սկսում է հետևել քեզ + Տեղեկացնել, երբ որևէ մեկը տարածում է թութդ + Տեղեկացնել, երբ որևէ մեկը հավանում է թութդ + Տեղեկացնել, երբ որևէ մեկը նշում է քեզ + Notify when a poll ended + Notify for new posts + Ցուցադրել հաստատման պատուհանը տարածելուց առաջ + Ցուցադրել հաստատման պատուհանը հավանածներին ավելացնելուց առաջ + Ծանուցել միայն WIFI-ին կպած + Ծանուցե՞լ + Լուռ ծանուցումներ + NSFW ցուցադրման տևողությունը(վրկյ-ով, 0-ն անջատելն է) + Media Description timeout (seconds, 0 means off) + Խմբագրել Էջը + Custom sharing + Your custom sharing URL… + Կենսագրություն… + Lock account + Պահել փոփոխությունները + Ընտրել գլխավոր նկար + Fit preview images + Automatically split toots in replies when chars are over: + Դու հատե՛լ ես թույլատրված 160 նիշերի սահմանը + Դու հատե՛լ ես թույլատրված 30 նիշերի սահմանը + Սկսած՝ + մինչև + Ժամը պետք է ավելի ուշ լինի, քան %1$s֊ը + Ժամը պետք է ավելի վաղ լինի, քան %1$s֊ը + Սկսելու ժամը + Ավարտի ժամը + Օգտագործել ներկառուցված զննիչը + Custom tabs + Թույլատրել JavaScript + Automatically expand cw + Թույլատրել երրորդ կողմի քուքիներ + Your API key, you can leave blank for Yandex + + Dark + Light + Black + + Կարգել LED-ի գույնը․ + + Կապույտ + Երկնագույն + Մանուշակագույն + Կանաչ + Կարմիր + Դեղին + Սպիտակ + + Հետևել + Ապաարգելափակել + Խլացնել + Ապախլացնել + Հայտն ուղարկված է + Հետևում է քեզ + Փնտրել + Առաջին տառը մեծատառով՝ երբ արձագանքում եմ + Չափափոխել նկարները + Resize videos + + Փուշ ծանուցումներ + Հաստատիր այն փուշ ծանուցումները, որոնք ցանկանում ես ստանալ։ + Հետագայում կարող ես անջատել կամ միացնել այս ծանուցումները կարգավորումներից։ + + + Մաքրել քեշը + Քեշում %1$s բովանդակություն կա։\n\nՑանկանում ես ջնջե՞լ + Մբ + Քեշը մաքուր է! %1$s տարածք ազատվեց + + Title + Title… + Description + Keywords + Keywords… + + Synchronize + Filter + Your toots + Your notifications + Public + Unlisted + Private + Direct + Some keywords… + Show media + Show pinned + No matching result found! + Backup toots for %1$s + %1$s new toots have been imported + %1$s new notifications have been imported + + Dates descending + Dates ascending + + + No + Only + Both + + No toots were found in database. Please, use the synchronize button from the menu to retrieve them. + + Հավաքած բովանդակություն + Հաշիվներից սարքում պահվում են զուտ հիմնական տեղեկությունները։ + Այս տեղեկությունները հույժ գաղտնի են և կարող են օգտագործվել միայն հավելվածի կողմից։ + Հավելվածը ջնջելու դեպքում այդ տեղեկություններն իսկույն կհեռացվեն սարքից։\n + ⚠ Ծածկանուններն ու գաղտնաբառերը երբեք չեն պահվում։ Դրանք օգտագործվում են միայն հանգույցի հետ ապահով իսկորոշման(SSL) ընթացքում։ + + Թույլտվություններ. + - ACCESS_NETWORK_STATE. Օգտագործվում է սարքի կապը WIFI-ին ստուգելու համար։\n + - INTERNET․ Օգտագործվում է հանգույցին հարցումներ ուղարկելու համար։\n + - WRITE_EXTERNAL_STORAGE․ Օգտագործվում է մեդիայի պահման կամ հավելվածն SD քարտ տեղափոխելու համար։\n + - READ_EXTERNAL_STORAGE․ Օգտագործվում է թթին մեդիա կցելու համար։\n + - BOOT_COMPLETED․ Օգտագործվում է ծանուցման ծառայությունը գործարկելու համար։\n + - WAKE_LOCK. Օգտագործվում է ծանուցման ծառայության մատուցման ընթացքում։ + + API-ի թույլտվություններ․ + -Կարդալ․ Կարդալ տվյալները։\n + -Գրել․ Գրառումներ անել և վերբեռնել մեդիա դրանց համար։\n + -Հետևել․ Հետևել, չհետևել, արգելափակել, ապաարգելափակել։\n\n + ⚠ Այս գործողություններն իրականացվում են միայն օգտատիրոջ հայցով։ + + Հսկում ու գրադարաններ + Հավելվածը չի՛ օգտագործում հսկող գործիքներ (լսարանի չափում, սխալանքների զեկուցում և այլն) և չի՛ պարունակում որևէ գովազդ։\n\n + Գրադարանների օգտագործումը հասցված է նվազագույնի՝\n + ֊Glide՝ մեդիան կառավարելու համար\n + ֊Android-Job՝ ծառայությունները կառավարելու համար\n + ֊PhotoView՝ նկրները կառավարելու համար\n + + Թթերի թարգմանություն + Հավելվածը հնարավորություն է ընձեռում թարգմանել թթերը՝ օգտագործելով սարքի տեղույթը և Յանդեքսի API-ը։\n + Յանդեքսն ունի սեփական գաղնիության քաղաքականությունը, որը կարող ես գտնել այստեղ․ https://yandex.ru/legal/confidential/?lang=en + Շնորհակալություն ներքոհիշյալներին․ + + Ֆիլտրել ըստ կանոնավոր արտահայտությունների + Փնտրել + Ջնջել + Նոր թթեր բերել… + + Ցանկեր + Վստա՞հ ես, որ ուզում ես ընդմիշտ ջնջել այս ցանկը + Այս Ցանկում դեռ ոչինչ չկա։ Երբ ցանկի անդամները նոր գրառումներ կատարեն, դրանք կհայտնվեն այստեղ։ + Ավելացնել ցանկին + Ցանկ ավելացնել + Ջնջել ցանկը + Փոփոխել ցանկը + Նոր ցանկի վերնագիր + The account was added to the list! + You don\'t have any lists yet! + + %1$s-ը տեղափոխվել է %2$s + Իսկորոշումը չի՞ աշխատում + Ահա մի քանի բան, որոնք կարող են օգնել․\n\n + Համոզվիր, որ ոչ մի սխալ թույլ չես տվել՝ հանգույցի անվան մեջ\n\n + Ստուգիր արդյոք քո հանգույցն անխափան աշխատում է, թե ոչ\n\n + Եթե օգտվում ես երկքայլ իսկորոշումից(2FA)՝ օգտագործիր ներքևի հղումը(հանգույցի անվանումը լրացնելուց հետո)\n\n + Կարող ես օգտագործել վերոհիշյալ հղումը նաև առանց երկքայլ իսկորոշումն օգտագործելու\n\n + Եթե այս ամենից հետո դեռ չի աշխատում, քեզ այլ բան չի մնում քան խնդրի մասին բարձրաձայնելը Github-ում՝ հետևյալ հղումով․ https://framagit.org/tom79/fedilab/issues + + Մեդիան բեռնվել է։ Կտտացրու այստեղ՝ այն ցուցադրելու համար։ + Այս գործողությունը բավական երկար կարող է տևել։ Դու կծանուցվես դրա ավարտի մասին։ + Դեռ բեռնվում է, սպասի՛ր… + Արտահանել գրառումները + Արտահանել գրառումները %1$s-ի համար + %2$s-ից %1$s թութ արտահանվել է։ + Ինչ-որ բան սխալ է գնացել %1$s-ի բովանդակությունն արտահանելու ընթացքում + Something went wrong when exporting data! + Something went wrong when importing data! + + Պրոքսի + Միացնե՞լ պրոքսին + Հոսթ + Պորտ + Մուտք + Գաղտնաբառ + Add toot details when sharing + Support the app on Liberapay + There is an error in the regular expression! + No timelines was found on this instance! + Delete this instance? + Translate in + Follow instance + You already follow this instance! + The instance is followed! + Partnerships + Information + Hide boosts from %s + Feature on profile + Show boosts from %s + Don\'t feature on profile + The account is now featured on profile + The account is no longer featured on profile + Boosts are now shown! + Boosts are now hidden! + Direct message + Filters + No filters to display. You can create one by tapping on the \"+\" button. + Keyword or phrase + Home timeline + Public timelines + Notifications + Conversations + Will be matched regardless of casing in text or content warning of a toot + Drop instead of hide + Filtered toots will disappear irreversibly, even if filter is later removed + When the keyword or phrase is alphanumeric only, it will only be applied if it matches the whole word + Whole word + Filter contexts + One or multiple contexts where the filter should apply + Expire after + Delete filter? + Update filter + Create filter + Whom to follow + There is no accounts listed for the moment! + Follow + Select all + Unselect all + %s is followed! + Creating the list %s + Adding accounts to the list + Accounts were added to the list + Adding accounts to the list + You have not created a list yet. Tap on the \"+\" button to add a new one. + Who to follow + Trunk API + Account(s) can\'t be followed + Fetching remote account + Automatically expand hidden media + New follow + New Boost + New Favourite + New Mention + Poll Ended + New Toot + Toots Backup + New posts + Media Download + Change notification sound + Select Tone + Enable time slot + How To Videos + Fetching remote thread! + No blocked domains! + Unblock domain + Are you sure to unblock %s? + Are you sure to block %s?\n\nYou will not see any content from that domain in any public timeline or in your notifications. Your followers from that domain will be removed. + Blocked domains + Block domain + The domain is blocked + The domain is no longer blocked! + Fetching remote status + Comment + Peertube instance + Be the first to leave a comment on this video with the top right button! + %s views + Duration: %s + Add an instance + Comments are not enabled on this video! + Pick up a resolution + Peertube favourites + The video has been added to bookmarks! + The video has been removed from bookmarks! + There is no Peertube videos in your favourites! + Channel + Videos + Channels + Use Emoji One + Information + Display previews in all toots + New UX/UI designer + Display video previews + The account id has been copied in the clipboard! + Change the language + Default language + Truncate long toots + Truncate toots over \'x\' lines. Zero means disabled. + Display more + Display less + Manage tags + The tag already exists! + The tag has been stored! + The tag has been changed! + The tag has been removed! + Schedule boost + The boost is scheduled! + No scheduled boost to display! + Schedule boost.]]> + Art timeline + Open menu + Go back + Logo of the application + Profile picture + Profile banner + Contact admin of the instance + Add new + MastoHost logo + Emoji picker + Refresh + Expand the conversation + Remove an account + Remove the blocked domain + Custom emoji picker + Play video + New toot + Image of the card + Hide media + Favicon + Add description for media (for the visually impaired) + + Never + 30 րոպե + 1 ժամ + 6 ժամ + 12 ժամ + 1 օր + 1 շաբաթ + + In this field, you need to write your instance host name.\nFor example, if you created your account on https://mastodon.social\nJust write mastodon.social (without https://)\n + You can start writing first letters and names will be suggested.\n\n + ⚠ The Login button will only work if the instance name is valid and the instance is up! + + More information + + Լեզուներ + Media only + Show NSFW + Crowdin translations + Crowdin manager + Translation of the application + About Crowdin + Bot + Pixelfed instance + Mastodon instance + Any of these + All of these + None of these + Any of these words (space-separated) + All these words (space-separated) + Add some words to filter (space-separated) + Change column name + Misskey instance + No app supporting this link is installed on your device. + Subscriptions + Overview + Trending + Recently added + Local + Upload + Reply + Delete a comment + Are you sure to delete this comment? + Full screen video + Mode for videos + Select the file to upload + My videos + Title + License + Category + Language + This video contains mature or explicit content + Enable video comments + Update video + Description + The video has been updated! + Upload cancelled! + The video has been uploaded! + Uploading, please wait… + Tap here to edit the video data. + Delete video + Are you sure to delete this video? + Display NSFW videos + No videos to display! + Leave a comment + Share + Choose a schedule mode + From device + From server + Toots (Server) + Toots (Device) + Modify + Display new toots above the \"Fetch more\" button + Timelines + Interface + Contacts + %1$s commented your video %2$s]]> + %1$s is following your channel %2$s]]> + %1$s is following your account]]> + %1$s has been published]]> + %1$s succeeded]]> + %1$s failed]]> + %1$s published a new video: %2$s]]> + %1$s has been blacklisted]]> + %1$s has been unblacklisted]]> + Export data + Import Data + Select the file to import + An error occurred when selecting the backup file! + Add a public comment + Send comment + There is no Internet connection. Your message has been stored in drafts. + Plain text + HTML + Markdown + Logout account + All + Support the app + Open Collective enables groups to quickly set up a collective, raise funds and manage them transparently. + Copy link + Connect + Normal + Compact + Console + Set display mode + Patch the Security Provider + Update tracking domains + The tracking data base has been updated! + http calls blocked by the application + List of blocked calls + Submit + The data base has been exported! + Featured hashtags + Filter timeline with tags + No tags + Hide the \"delete\" button in the notification tab + Attach an image when sharing a URL + + Poll + Polls + Create a poll + Choice 1 + Choice 2 + Choice %d + You need two choices at least for the poll! + Done + end at %s + Refresh poll + Vote + A poll you have voted in has ended + A poll you tooted has ended + Customize + Categories + Time slot + Advanced + Display \'new\' badge on unread toots + Peertube + Move timeline + Hide timeline + Reorder timelines + List permanently deleted + Followed instance removed + Pinned tag removed + Undo + You need to keep two visible tabs! + Reorder timelines + Main timelines can only be hidden! + BBCode + Always mark media as sensitive + GNU instance + Cached status + Forward tags in replies + Long press to store media + Blur sensitive media + Display timelines in a list + Display timelines + Mark bot accounts in toots + Manage tags + Remember the position in Home timeline + History + Playlists + Display name + You don\'t have any playlists. Tap on the \"+\" icon to add a new playlist + You must provide a display name! + The channel is required when the playlist is public. + Create a playlist + There is nothing in this playlist yet. + redo + Gallery + Emoji + Sticker + Eraser + Text + Filter + Brush + Are you sure you want to exit without saving the image? + Discard + Saving… + Image Saved Successfully! + Failed to save Image + Opacity + Enable photo editor + Add a poll item + Remove last poll item + Mute conversation + Unmute conversation + The conversation is no longer muted! + The conversation is muted + Open application features + Timed mute + Mention the account + Refresh cache + Mention the status + News + General + Regional + Art + Journalism + Activism + Gaming + Technology + Adult content + Furry + Food + Logo of the instance + Something went wrong when checking available instances! + Join Mastodon + Choose an instance by picking up a category, then tap on a check button. + Choose an instance by tapping on a check button. + %1$s users + Confirm password + I agree to %1$s and %2$s + server rules + terms of service + Sign up + This instance works with invitations. Your account will need to be manually approved by an administrator before being usable. + Please, fill all the fields! + Passwords don\'t match! + The email doesn\'t seem to be valid! + Your username will be unique on %1$s + You will be sent a confirmation e-mail + Use at least 8 characters + Password should contain at least 8 characters + Username should only contain letters, numbers and underscores + Account created! + Your account has been created!\n\n + Think to validate your email within the 48 next hours.\n\n + You can now connect your account by writing %1$s in the first field and click on Connect.\n\n + Important: If your instance required validation, you will receive an email once it is validated! + + Save the message in drafts? + Administration + Reports + No reports to display! + Reconnect the account + The application failed to access the admin features. You might need to reconnect the account for having the correct scope. + Unresolved + Remote + Active + Pending + Disabled + Silenced + Suspended + Permissions + Email status + Login status + Joined + Most recent IP + Warn + Disable + Silence + Notify the user per e-mail + Custom warning + User + Moderator + Administrator + Confirmed + Not confirmed + Reported statuses + Account + Undo silence + Undo disable + Suspend + Undo suspend + The account is silenced! + The account is no longer silenced! + The account is suspended! + The account is no longer suspended! + The account is disabled! + The account is no longer disabled! + The account has been warned! + Display the admin menu + Display the admin feature in statuses + Allow + The account is approved! + The account is rejected! + Assign to me + Unassign + Mark as resolved + Mark as unresolved + Empty content! + Display Fedilab features button + The application needs to access audio recording + Voice message + Enable quick reply + The account you are replying might not see your message! + If disabled, the app will always load last statuses + If disabled, sensitive media will be hidden with a button + Store media in full size with a long press on previews + Add an ellipse button at the top right for listing all tags/instances/lists + During the time slot, the app will send notifications. You can reverse (ie: silent) this time slot with the right spinner. + Display a Fedilab button below profile picture. It is a shortcut for accessing in-app features. + Allow to reply directly in timelines below statuses + Previews will not be cropped in timelines + Allow to play embedded videos directly in timelines + Allow to reverse the way to read statuses that are displayed once tapping the fetch more button + This option allows to support recent cipher suites. It is useful for older Android devices or if you cannot connect to your instance. + Exclusively for Peertube videos. Switch this mode if you cannot play them. + These tags will allow to filter statuses from profiles. You will have to use the context menu for seeing them. + Automatically insert a line break after the mention to capitalize the first letter + Allow content creators to share statuses to their RSS feeds + Compose + Maximum retry times when uploading media + Create a new Folder here + Enter the folder name + Please enter a valid folder name + This folder already exists.\n Please provide another name for the folder + Select + Default Directory + Folder + Create folder + Display a toast message after an action has been completed (boost, fav, etc.)? + Muted instances have been exported! + Add an instance + Export instances + Import instances + Crash reports + Enable crash reports + If enabled, a crash report will be created locally and then you will be able to share it. + Fedilab has stopped :( + You can send me by email the crash report. It will help to fix it :)\n\nYou can add additional content. Thank you! + Use the wysiwyg + When enabled, you will be able to format your text easily with tools. + Statistics + Total statuses + Number of boosts + Number of favourites + Number of mentions + Number of follows + Number of polls + Number of replies + Number of statuses + Statuses + Visibility + Number with media + Number with sensitive media + Number with CW + First status date + Last status date + First notification date + Last notification date + Frequency + %s statuses per day + %s notifications per day + Date range + Groups + No groups! + Disable custom animated emojis + Charts + Display charts + The application collects your local data, please wait… + Backup + Auto backup statuses + This option is per account. It will launch a service that will automatically store your statuses locally in the database. That allows to get statistics and charts + Auto backup notifications + This option is per account. It will launch a service that will automatically store your notifications locally in the database. That allows to get statistics and charts + Report account + Send an invitation + Your instance does not allow to register a new account! + + %d vote + %d votes + + + %d voter + %d voters + + + Single choice + Multiple choices + + + 5 minutes + 30 minutes + 1 hour + 6 hours + 1 day + 3 days + 7 days + + + Webview + Direct stream + + For joining my instance \"%1$s\", you can download Fedilab:\n\nF-Droid: %2$s\nGoogle: %3$s\n\nThen open the link below with Fedilab and create your account :)\n\n%4$s + + Your poll can\'t have duplicated options! + For all accounts + Database cache + Clear your home timeline cache + Clear your cached statuses + Clear your bookmarks + Files in cache + Total notifications + Hide menu items + Fedilab is running live notifications + For %1$s accounts with %2$s events + Live notifications for %1$s + Live notifications will be enabled for this account. + Clear cache when leaving + The cache (media, cached messages, data from the built-in browser) will be automatically cleared when leaving the application. + Do you want to unfollow this account? + Show confirmation dialog before unfollowing + Replace Youtube with Invidio.us + Invidious is an alternative front-end to YouTube + Enter your custom host or leave blank for using invidious.snopyta.org + Replace Twitter with Nitter + Nitter is an open source alternative Twitter front-end focused on privacy. + Enter your custom host or leave blank for using nitter.net + Replace Instagram with Bibliogram + Bibliogram is an open source alternative Instagram front-end focused on privacy. + Enter your custom host or leave blank for using bibliogram.art + Replace Reddit with Libreddit + Libreddit is an open source alternative Reddit front-end focused on privacy. + Enter your custom host or leave blank for using libredd.it + Replace Medium links + Replace medium.com links with an open source alternative front-end focused on privacy. + Default: scribe.rip + Replace Wikipedia links + Replace Wikipedia link with an open source alternative front-end focused on privacy. + Default: wikiless.org + Hide Fedilab notification bar + For hiding the remaining notification in the status bar, tap on the eye icon button then uncheck: \"Display in status bar\" + Use a push notifications system for getting notifications in real time. + No live notifications + Live notifications + Notifications will be fetched every 15 minutes. + Add notes + Notes for the account + Allow to compress large photos into smaller sized photos with very less or negligible loss in quality of the image. + Allow compressing videos while maintaining their quality. + The app is compressing the media, it can be quite long… + Change app icon + Tap to change the app icon + Post + Visibility of the post + Tap here to add photos + Accepted Formats: jpeg, png, gif \n\nMax File Size: 15 MB \n\nAlbums can contain up to 4 photos or videos + Upload media + Add an optional caption + The app received a very long error message from the API %1$s + Message preview + Add mentions in each message + Fetching conversation + Order by + Title for the video + Join Peertube + I am at least 16 years old and agree to the %1$s of this instance + Links + Change the color of links (URLs, mentions, tags, etc.) in messages + Reblogs header + Change the color of display name at the top of messages + Change the color of the user name at the top of messages + Change the color of the header for reblogs + Posts + Background color of posts in timelines + Reset colors + Tap here to reset all your custom colors + Reset + Icons + Color of bottom icons in timelines + Pin this tag + Logo of the instance + Edit profile + Make an action + Translation + Image preview + Text color + Change the text color in pots + Apply changes + You need to restart the application to apply changes + Restart + Use a custom theme + Allow to override colors of the selected theme above + Theming + Store before + The theme was exported + The theme has been successfully exported in CSV + Apply the primary color to the status bar + Status bar color + Restore a default theme + Import a theme + Tap here to import a theme from a previous export + Export the theme + Tap here to export the current theme + An error occurred when selecting the theme file + Theme Picker + Select a pre-installed theme + Themes + Apply the primary color to the navigation bar + Navigation bar color + The underlying color of the app’s content. + Background color + Accents select parts of the UI. + Accent color + Displayed most frequently across your app. + Primary color + Export bookmarks to the instance + Import bookmarks from the instance + User count + Status count + Instance count + Blocked + End in %s + What\'s new in %s + You can follow my account for updates + This instance is not available on https://instances.social + Display full link + Share link + The URL has been copied to the clipboard + Open with another app + Check redirect + This URL does not redirect + %1$s \n\nredirects to\n\n %2$s + Change the user agent + Set a custom user agent or leave blank + Allows to customize the user agent used for api calls or with the built-in browser. + Remove UTM parameters + The app will automatically remove UTM parameters from URLs before visiting a link. + Trends + Trending now + %d people talking + Twitter accounts (via Nitter) + Twitter usernames space separated + Identity proofs + Verified identity + Verified by %1$s (%2$s) + Delete the notification + Display more options + It is a Pixelfed story + Upload a media, it will be automatically added to your Pixelfed story. + Media successfully added to your story! + Action disabled + Unfollow + Something went wrong, please check your download directory in settings. + Announcements + No announcements! + Add a reaction + Use your favourite browser inside the app. Uncheck this feature to open links externally. + Video cache in MB, zero means no cache. + Watermarks + Automatically add a watermark at the bottom of pictures. The text can be customized for each account. + No distributors found! + You need a distributor for receiving push notifications.\nYou will find more details at %1$s.\n\nYou can also disable push notifications in settings for ignoring that message. + Select a distributor + diff --git a/app/src/main/res/values-id/strings.xml b/app/src/main/res/values-id/strings.xml new file mode 100644 index 000000000..45d9b5c47 --- /dev/null +++ b/app/src/main/res/values-id/strings.xml @@ -0,0 +1,1136 @@ + + + Buka menu + Tutup menu + Tentang + Tentang contoh + Pribadi + Singgahan + Keluar + Masuk + + Tutup + Iya + Tidak + Batal + Unduh + Unduh %1$s + Media disimpan + Berkas: %1$s + Kata Sandi + Surel + Akun + Kutipan + Label + Simpan + Mengembalikan + Tidak ada hasil! + Contoh + Contoh: mastodon.sosial + Sekarang bekerja dengan akun %1$s + Tambahkan sebagai akun + Isi tiupan telah disalin ke papan klip + The URL of the toot has been copied to the clipboard + Perubahan + Memilih gambar… + Bersih + Kamera + Hapus semua + Terjemahkan modul ini. + Susunan acara + Text and icon sizes + Ubah ukuran text saat ini: + Ubah ukuran ikon saat ini: + Selanjutnya + Sebelumnya + Buka dengan + Mengesahkan + Media + Bagikan dengan + Dibagi melalui Fedilab + Balasan + Nama pengguna + Konsep + Favorit + Pengikut baru + Menyebutkan + Tingkatkan + Tampilkan Tingkatan + Tampilkan balasan + Buka di peramban + Menerjemahkan + Mohon tunggu beberapa detik sebelum membuat tindakan. + + Beranda + Jadwal kronologi + Federasi kronologi + Opsi + Favorit + Komunikasi + Pengguna diredam + Pengguna diblokir + Pemberitahuan + Permintaan mengikuti + Pengaturan + Menghapus akun + Hapus akun %1$s dari aplikasi? + Kirim sebuah surel + Klik di Jalur untuk mengubahnya + Gagal! + Kutipan terjadwal + Informasi di bawah ini mungkin mencerminkan profil pengguna tidak lengkap. + Masukkan karakter + Aplikasi tidak mengumpulkan kostum karakter untuk saat ini. + Push notifications + Are you sure you want to logout? + Are you sure you want to logout @%1$s@%2$s? + + Tidak ada kutipan untuk ditampilkan + No stories to display + Stories + Ditingkatkan sebesar %1$s + Tambahkan kutipan ini ke favorit kamu? + Hapus kutipan ini dari kesukaan Anda? + Tingkatkan kutipan ini? + Lepaskan tingkatan kutipan ini? + Sematkan kutipan ini? + Lepaskan sematan kutipan ini? + Bisu + Blok + Melaporkan + Menghapus + Salin + Bagikan + Menyebut + Mematikan waktu + Hapus & draf ulang + + Bisukan akun ini? + Blok akun ini? + Laporkan kutipan ini? + Blok domain ini? + Unmute this account? + Unblock this account? + + + Notifikasi + Senyap + + + Menghapus kutipan ini? + Delete & re-draft this toot? + + Bookmark + Tambahkan ke bookmark + Hapus bookmark + Tidak ada bookmark untuk ditampilkan + Status telah ditambahkan ke bookmark! + Status telah dihapus dari bookmark! + + %d s + %d m + %d h + %d d + + %d detik + + + %d menit + + + %d jam + + + %d hari + + + Peringatan + Apa yang Anda pikirkan? + KUTIPAN! + QUEET! + cw + Tulis sebuah kutipan + Balas ke sebuah kutipan + Tulis sebuah queet + Balas sebuah queet + Pilih media + Terjadi kesalahan saat memilih media! + Hapus media ini? + Kutipan Anda kosong! + Visibilitas dari kutipan + Visibilitas kutipan secara standar: + Kutipan telah dikirim! + Anda membalas kutipan ini: + Konten sensitif? + + Posting ke kronologi umum + Jangan posting ke kronologi umum + Posting ke pengikut saja + Posting ke pengguna yang disebutkan saja + + Tidak ada konsep! + Pilih kutipan + Pilih akun + Pilih beberapa akun + Menghapus konsep? + Klik pada tombol untuk menampilkan kutipan asli + Menjelaskan untuk tuna netra + + Deskripsi tidak tersedia! + + Rilis %1$s + Pengembang: + Lisensi: + GNU GPL V3 + Sumber kode: + Terjemahan dari kutipan: + Contoh pencarian: + Perancang ikon: + + Percakapan + + Tidak ada akun yang ditampilkan + Tidak ada permintaan mengikuti + Kutipan \n %1$s + Pengikut \n %1$s + Pengikut \n %1$s + Disematkan \n %d + Memberi hak + Tolak + + Tidak ada jadwal kutipan untuk ditampilkan! + Tuliskan kutipan lalu pilih Susunan acara dari menu paling atas. + Hapus kutipan terjadwal? + Media: %d + Kutipan yang telah dijadwalkan! + Tanggal yang dijadwalkan harus lebih besar dari jam saat ini! + Penghemat baterai diaktifkan! Ini mungkin tidak bekerja seperti yang diharapkan. + + Untuk mematikan waktu harus lebih besar dari satu menit. + %1$s telah dibisukan sampai %2$s.\n Kamu dapat menyalakan akun ini dari halaman profil mereka. + %1$s dibisukan sampai %2$s.\n Klik disini untuk menyalakan akun. + + Tidak ada pemberitahuan untuk ditampilkan + menyebutmu + wrote a new message + tingkatkan status Kamu + status kesukaan Kamu + mengikutimu + meminta untuk mengikuti anda + + dan %d pemberitahuan lainnya + + + %d like + + Hapus pemberitahuan? + Hapus semua pemberitahuan? + Pemberitahuan telah dihapus! + Semua pemberitahuan telah dihapus! + + Yang diikuti + Pengikut + Disematkan + + Tidak menemukan id klien! + Tidak dapat terhubung ke instance domain! + Tidak ada koneksi internet! + Akun diblokir! + Akun tidak lagi diblokir! + Akun dibisukan! + Akun tidak dibisukan lagi! + Akun itu diikuti! + Akun tersebut tidak lagi diikuti! + Kutipan itu ditingkatkan! + Kutipan tidak lagi ditingkatkan! + Kutipan itu ditambahkan ke favoritmu! + Kutipan itu telah dihapus dari favoritmu! + Kutipan itu telah dilaporkan! + Kutipan itu telah dihapus! + Kutipan itu disematkan! + Kutipan itu dilepas sematnya! + Ups! Terjadi kesalahan! + Terjadi Kesalahan! Contohnya tidak mengembalikan kode otoritas! + Domain contoh sepertinya tidak valid! + Terjadi kesalahan saat beralih akun! + Terjadi kesalahan saat mencari! + Data profil telah disimpan! + Tidak dapat diambil tindakan + Media telah tersimpan! + Terjadi kesalahan saat menerjemahkan! + Terjemahan dinonaktifkan di pengaturan + Konsep tersimpan! + Apakah anda yakin contoh ini memungkinkan jumlah karakter ini? biasanya, nilai mendekati 500 karakter. + Visibilitas kutipan telah diubah untuk akun %1$s + + Jumlah kutipan per beban + Selalu + WIFI + Menanyakan + Memuat media + Memuat gambar + Tampilkan lebih banyak… + Tampilkan sedikit… + Konten sensitif + Nonaktifkan avatar GIF + Jalur: + Simpan konsep otomatis + Tambahkan URL dari media kutipan + Beritahu ketika seseorang mengikuti Anda + Beritahu ketika seseorang meningkatkan status Anda + Beritahu ketika seseorang menyukai status Anda + Beritahu ketika seseorang menyebut Anda + Beritahu ketika pemilihan berakhir + Notify for new posts + Tampilkan dialog konfirmasi sebelum meningkatkan + Tampilkan dialog konfirmasi sebelum menambahkan ke favorit + Beritahu hanya di WIFI + Memberitahu? + Diamkan pemberitahuan + Lihat NSFW waktu habis (detik, 0 berarti mati) + Waktu Keterangan Media habis (detik, 0 berarti mati) + Ubah profil + Custom sharing + Your custom sharing URL… + Biodata… + Kunci akun + Simpan perubahan + Pilih gambar header + Fit preview images + Pisah toots secara otomatis di balasan ketika karakter lebih dari: + Kamu telah mencapai 160 karakter yang diizinkan! + Kamu telah mencapai 30 karakter yang diizinkan! + Antara + dan + Waktunya harus lebih besar dari %1$s + Waktunya harus lebih rendah dari %1$s + Waktu mulai + Waktu akhir + Gunakan browser built-in + Tab kustom + Aktifkan Javascript + Automatically expand cw + Izinkan cookie pihak ketiga + Your API key, you can leave blank for Yandex + + Dark + Light + Hitam + + Atur warna LED: + + Biru + Sian + Magenta + Hijau + Merah + Kuning + Putih + + Ikuti + Buka blokir + Bisu + Bersuara + Kirim permintaan + Mengikutimu + Pencarian + Huruf pertama kapital untuk membalas + Resize pictures + Resize videos + + Tolak pemberitahuan + Tolong konfirmasi penolakan pemberitahuan yang ingin Anda terima. + Anda dapat mengaktifkan atau menonaktifkan pemberitahuan ini di setelan (Tab pemberitahuan). + + + Kosongkan cache + Ada %1$s data dalam cache.\n\nApakah Anda ingin menghapusnya? + Mb + Cache sudah dikosongkan! %1$s dilepaskan + + Judul + Title… + Description + Keywords + Keywords… + + Synchronize + Filter + Your toots + Your notifications + Public + Unlisted + Private + Direct + Some keywords… + Show media + Show pinned + No matching result found! + Backup toots for %1$s + %1$s new toots have been imported + %1$s new notifications have been imported + + Dates descending + Dates ascending + + + No + Hanya + Both + + No toots were found in database. Please, use the synchronize button from the menu to retrieve them. + + Data terekam + Hanya informasi dasar dari akun yang tersimpan pada perangkat. + Data ini sangat rahasia dan hanya bisa digunakan oleh aplikasi. + Menghapus aplikasi segera menghapus data ini.\n + ⚠ masuk dan kata sandi tidak pernah disimpan. Mereka hanya digunakan selama otentikasi aman (SSL) dengan sebuah tuntutan. + + Izin: + - ACCESS_NETWORK_STATE: Digunakan untuk mendeteksi perangkat terhubung ke jaringan WIFI.\n + - INTERNET: Digunakan untuk menanyakan suatu hal.\n + - WRITE_EXTERNAL_STORAGE: Digunakan untuk menyimpan media atau memindahkan aplikasi ke kartu SD.\n + - READ_EXTERNAL_STORAGE: Digunakan untuk menambahkan media ke kutipan.\n + - BOOT_COMPLETED: Digunakan untuk memulai layanan pemberitahuan.\n + - WAKE_LOCK: Digunakan selama layanan pemberitahuan. + + Izin API: + - Read: Membaca data.\ + - Write: Posting status dan unggah media untuk status.\n + - Follow: Ikuti, berhenti ikuti, blokir, buka blokir.\n\n + ⚠ Tindakan ini hanya saat pengguna memintanya. + + Pelacakan dan Perpustakaan + Aplikasi tidak menggunakan alat pelacak (pengukuran pemirsa, kesalahan pelaporan, dll.) dan tidak berisi iklan apapun.\n\n + Penggunaan perpustakaan diminimalkan: \n + - Luncuran: Untuk mengelola media\n + - Kerja Android: Untuk mengelola layanan\n + - Tampilan Foto: Untuk mengelola gambar\n + + Terjemahan dari kutipan + Aplikasi ini menawarkan kemampuan untuk menerjemahkan kutipan menggunakan perangkat lokal dan API Yandex.\n + Yandex memiliki kebijakan privasi yang tepat yang dapat ditemukan disini: +https://yandex.ru/legal/confidential/?lang=en + + Terima kasih untuk: + + Saring dengan ekspresi reguler + Cari + Hapus + Ambil lebih banyak kutipan… + + Daftar + Apakah Anda yakin ingin menghapus secara permanen daftar ini? + Belum ada dalam daftar ini. Saat Anggota daftar ini memposting status baru, mereka akan muncul disini. + Tambahkan ke daftar + Tambahkan daftar + Hapus daftar + Edit daftar + Judul daftar baru + The account was added to the list! + You don\'t have any lists yet! + + %1$s telah dipindahkan ke%2$s + Otentikasi tidak bekerja? + Disini ada beberapa pemeriksaan untuk membantu: +\n\n + - Periksa bahwa tidak ada kealahan dalam pengejaan contoh nama\n\n + - Periksa bahwa contoh tidak down\n\n + - Jika anda menggunakan two-factor authentication (2FA), gunakan link dibawah ini (begitu nama contoh terisi)\n\n + - Anda juga dapat menggunakan link tersebut tanpa menggunakan 2FA\n\n + - Jika hal tersebut tidak bekerja, anda dapat melaporkan masalah tersebut pada Github di https://github.com/stom79/mastalab/issues + + Media telah berhasil dimuat. Klik disini untuk menampilkannya. + Tindakan cukup lama, Anda akan mendapatkan notifikasi ketika tindakan ini telah selesai. + Masih berjalan, tunggu sebentar… + Status ekspor + Status ekspor untuk %1$s + %1$s panjang dari %2$s telah diekspor. + Terdapat kesalahan saat mengekspor data untuk %1$s + Something went wrong when exporting data! + Something went wrong when importing data! + + Proxy + Enable proxy? + Host + Port + Login + Password + Add toot details when sharing + Support the app on Liberapay + There is an error in the regular expression! + No timelines was found on this instance! + Delete this instance? + Translate in + Follow instance + You already follow this instance! + The instance is followed! + Partnerships + Information + Hide boosts from %s + Feature on profile + Show boosts from %s + Don\'t feature on profile + The account is now featured on profile + The account is no longer featured on profile + Boosts are now shown! + Boosts are now hidden! + Direct message + Filters + No filters to display. You can create one by tapping on the \"+\" button. + Keyword or phrase + Home timeline + Public timelines + Notifications + Conversations + Will be matched regardless of casing in text or content warning of a toot + Drop instead of hide + Filtered toots will disappear irreversibly, even if filter is later removed + When the keyword or phrase is alphanumeric only, it will only be applied if it matches the whole word + Whole word + Filter contexts + One or multiple contexts where the filter should apply + Expire after + Delete filter? + Update filter + Create filter + Whom to follow + There is no accounts listed for the moment! + Follow + Select all + Unselect all + %s is followed! + Creating the list %s + Adding accounts to the list + Accounts were added to the list + Adding accounts to the list + You have not created a list yet. Tap on the \"+\" button to add a new one. + Who to follow + Trunk API + Account(s) can\'t be followed + Fetching remote account + Automatically expand hidden media + New follow + New Boost + New Favourite + New Mention + Poll Ended + New Toot + Toots Backup + New posts + Media Download + Change notification sound + Select Tone + Enable time slot + How To Videos + Fetching remote thread! + No blocked domains! + Unblock domain + Are you sure to unblock %s? + Are you sure to block %s?\n\nYou will not see any content from that domain in any public timeline or in your notifications. Your followers from that domain will be removed. + Blocked domains + Block domain + The domain is blocked + The domain is no longer blocked! + Fetching remote status + Comment + Peertube instance + Be the first to leave a comment on this video with the top right button! + %s views + Duration: %s + Add an instance + Comments are not enabled on this video! + Pick up a resolution + Peertube favourites + The video has been added to bookmarks! + The video has been removed from bookmarks! + There is no Peertube videos in your favourites! + Channel + Videos + Channels + Use Emoji One + Information + Display previews in all toots + New UX/UI designer + Display video previews + The account id has been copied in the clipboard! + Change the language + Default language + Truncate long toots + Truncate toots over \'x\' lines. Zero means disabled. + Display more + Display less + Manage tags + The tag already exists! + The tag has been stored! + The tag has been changed! + The tag has been removed! + Schedule boost + The boost is scheduled! + No scheduled boost to display! + Schedule boost.]]> + Art timeline + Open menu + Go back + Logo of the application + Profile picture + Profile banner + Contact admin of the instance + Add new + MastoHost logo + Emoji picker + Refresh + Expand the conversation + Remove an account + Remove the blocked domain + Custom emoji picker + Play video + New toot + Image of the card + Hide media + Favicon + Add description for media (for the visually impaired) + + Never + 30 minutes + 1 hour + 6 hours + 12 hours + 1 day + 1 week + + In this field, you need to write your instance host name.\nFor example, if you created your account on https://mastodon.social\nJust write mastodon.social (without https://)\n + You can start writing first letters and names will be suggested.\n\n + ⚠ The Login button will only work if the instance name is valid and the instance is up! + + More information + + Languages + Media only + Show NSFW + Crowdin translations + Crowdin manager + Translation of the application + About Crowdin + Bot + Pixelfed instance + Mastodon instance + Any of these + All of these + None of these + Any of these words (space-separated) + All these words (space-separated) + Add some words to filter (space-separated) + Change column name + Misskey instance + No app supporting this link is installed on your device. + Subscriptions + Overview + Trending + Recently added + Local + Upload + Reply + Delete a comment + Are you sure to delete this comment? + Full screen video + Mode for videos + Select the file to upload + My videos + Title + License + Category + Language + This video contains mature or explicit content + Enable video comments + Update video + Description + The video has been updated! + Upload cancelled! + The video has been uploaded! + Uploading, please wait… + Tap here to edit the video data. + Delete video + Are you sure to delete this video? + Display NSFW videos + No videos to display! + Leave a comment + Share + Choose a schedule mode + From device + From server + Toots (Server) + Toots (Device) + Modify + Display new toots above the \"Fetch more\" button + Timelines + Interface + Contacts + %1$s commented your video %2$s]]> + %1$s is following your channel %2$s]]> + %1$s is following your account]]> + %1$s has been published]]> + %1$s succeeded]]> + %1$s failed]]> + %1$s published a new video: %2$s]]> + %1$s has been blacklisted]]> + %1$s has been unblacklisted]]> + Export data + Import Data + Select the file to import + An error occurred when selecting the backup file! + Add a public comment + Send comment + There is no Internet connection. Your message has been stored in drafts. + Plain text + HTML + Markdown + Logout account + All + Support the app + Open Collective enables groups to quickly set up a collective, raise funds and manage them transparently. + Copy link + Connect + Normal + Compact + Console + Set display mode + Patch the Security Provider + Update tracking domains + The tracking data base has been updated! + http calls blocked by the application + List of blocked calls + Submit + The data base has been exported! + Featured hashtags + Filter timeline with tags + No tags + Hide the \"delete\" button in the notification tab + Attach an image when sharing a URL + + Poll + Polls + Create a poll + Choice 1 + Choice 2 + Choice %d + You need two choices at least for the poll! + Done + end at %s + Refresh poll + Vote + A poll you have voted in has ended + A poll you tooted has ended + Customize + Categories + Time slot + Advanced + Display \'new\' badge on unread toots + Peertube + Move timeline + Hide timeline + Reorder timelines + List permanently deleted + Followed instance removed + Pinned tag removed + Undo + You need to keep two visible tabs! + Reorder timelines + Main timelines can only be hidden! + BBCode + Always mark media as sensitive + GNU instance + Cached status + Forward tags in replies + Long press to store media + Blur sensitive media + Display timelines in a list + Display timelines + Mark bot accounts in toots + Manage tags + Remember the position in Home timeline + History + Playlists + Display name + You don\'t have any playlists. Tap on the \"+\" icon to add a new playlist + You must provide a display name! + The channel is required when the playlist is public. + Create a playlist + There is nothing in this playlist yet. + redo + Gallery + Emoji + Sticker + Eraser + Text + Filter + Brush + Are you sure you want to exit without saving the image? + Discard + Saving… + Image Saved Successfully! + Failed to save Image + Opacity + Enable photo editor + Add a poll item + Remove last poll item + Mute conversation + Unmute conversation + The conversation is no longer muted! + The conversation is muted + Open application features + Timed mute + Mention the account + Refresh cache + Mention the status + News + General + Regional + Art + Journalism + Activism + Gaming + Technology + Adult content + Furry + Food + Logo of the instance + Something went wrong when checking available instances! + Join Mastodon + Choose an instance by picking up a category, then tap on a check button. + Choose an instance by tapping on a check button. + %1$s users + Confirm password + I agree to %1$s and %2$s + server rules + terms of service + Sign up + This instance works with invitations. Your account will need to be manually approved by an administrator before being usable. + Please, fill all the fields! + Passwords don\'t match! + The email doesn\'t seem to be valid! + Your username will be unique on %1$s + You will be sent a confirmation e-mail + Use at least 8 characters + Password should contain at least 8 characters + Username should only contain letters, numbers and underscores + Account created! + Your account has been created!\n\n + Think to validate your email within the 48 next hours.\n\n + You can now connect your account by writing %1$s in the first field and click on Connect.\n\n + Important: If your instance required validation, you will receive an email once it is validated! + + Save the message in drafts? + Administration + Reports + No reports to display! + Reconnect the account + The application failed to access the admin features. You might need to reconnect the account for having the correct scope. + Unresolved + Remote + Active + Pending + Disabled + Silenced + Suspended + Permissions + Email status + Login status + Joined + Most recent IP + Warn + Disable + Silence + Notify the user per e-mail + Custom warning + User + Moderator + Administrator + Confirmed + Not confirmed + Reported statuses + Account + Undo silence + Undo disable + Suspend + Undo suspend + The account is silenced! + The account is no longer silenced! + The account is suspended! + The account is no longer suspended! + The account is disabled! + The account is no longer disabled! + The account has been warned! + Display the admin menu + Display the admin feature in statuses + Allow + The account is approved! + The account is rejected! + Assign to me + Unassign + Mark as resolved + Mark as unresolved + Empty content! + Display Fedilab features button + The application needs to access audio recording + Voice message + Enable quick reply + The account you are replying might not see your message! + If disabled, the app will always load last statuses + If disabled, sensitive media will be hidden with a button + Store media in full size with a long press on previews + Add an ellipse button at the top right for listing all tags/instances/lists + During the time slot, the app will send notifications. You can reverse (ie: silent) this time slot with the right spinner. + Display a Fedilab button below profile picture. It is a shortcut for accessing in-app features. + Allow to reply directly in timelines below statuses + Previews will not be cropped in timelines + Allow to play embedded videos directly in timelines + Allow to reverse the way to read statuses that are displayed once tapping the fetch more button + This option allows to support recent cipher suites. It is useful for older Android devices or if you cannot connect to your instance. + Exclusively for Peertube videos. Switch this mode if you cannot play them. + These tags will allow to filter statuses from profiles. You will have to use the context menu for seeing them. + Automatically insert a line break after the mention to capitalize the first letter + Allow content creators to share statuses to their RSS feeds + Compose + Maximum retry times when uploading media + Create a new Folder here + Enter the folder name + Please enter a valid folder name + This folder already exists.\n Please provide another name for the folder + Select + Default Directory + Folder + Create folder + Display a toast message after an action has been completed (boost, fav, etc.)? + Muted instances have been exported! + Add an instance + Export instances + Import instances + Crash reports + Enable crash reports + If enabled, a crash report will be created locally and then you will be able to share it. + Fedilab has stopped :( + You can send me by email the crash report. It will help to fix it :)\n\nYou can add additional content. Thank you! + Use the wysiwyg + When enabled, you will be able to format your text easily with tools. + Statistics + Total statuses + Number of boosts + Number of favourites + Number of mentions + Number of follows + Number of polls + Number of replies + Number of statuses + Statuses + Visibility + Number with media + Number with sensitive media + Number with CW + First status date + Last status date + First notification date + Last notification date + Frequency + %s statuses per day + %s notifications per day + Date range + Groups + No groups! + Disable custom animated emojis + Charts + Display charts + The application collects your local data, please wait… + Backup + Auto backup statuses + This option is per account. It will launch a service that will automatically store your statuses locally in the database. That allows to get statistics and charts + Auto backup notifications + This option is per account. It will launch a service that will automatically store your notifications locally in the database. That allows to get statistics and charts + Report account + Send an invitation + Your instance does not allow to register a new account! + + %d votes + + + %d voters + + + Single choice + Multiple choices + + + 5 minutes + 30 minutes + 1 hour + 6 hours + 1 day + 3 days + 7 days + + + Webview + Direct stream + + For joining my instance \"%1$s\", you can download Fedilab:\n\nF-Droid: %2$s\nGoogle: %3$s\n\nThen open the link below with Fedilab and create your account :)\n\n%4$s + + Your poll can\'t have duplicated options! + For all accounts + Database cache + Clear your home timeline cache + Clear your cached statuses + Clear your bookmarks + Files in cache + Total notifications + Hide menu items + Fedilab is running live notifications + For %1$s accounts with %2$s events + Live notifications for %1$s + Live notifications will be enabled for this account. + Clear cache when leaving + The cache (media, cached messages, data from the built-in browser) will be automatically cleared when leaving the application. + Do you want to unfollow this account? + Show confirmation dialog before unfollowing + Replace Youtube with Invidio.us + Invidious is an alternative front-end to YouTube + Enter your custom host or leave blank for using invidious.snopyta.org + Replace Twitter with Nitter + Nitter is an open source alternative Twitter front-end focused on privacy. + Enter your custom host or leave blank for using nitter.net + Replace Instagram with Bibliogram + Bibliogram is an open source alternative Instagram front-end focused on privacy. + Enter your custom host or leave blank for using bibliogram.art + Replace Reddit with Libreddit + Libreddit is an open source alternative Reddit front-end focused on privacy. + Enter your custom host or leave blank for using libredd.it + Replace Medium links + Replace medium.com links with an open source alternative front-end focused on privacy. + Default: scribe.rip + Replace Wikipedia links + Replace Wikipedia link with an open source alternative front-end focused on privacy. + Default: wikiless.org + Hide Fedilab notification bar + For hiding the remaining notification in the status bar, tap on the eye icon button then uncheck: \"Display in status bar\" + Use a push notifications system for getting notifications in real time. + No live notifications + Live notifications + Notifications will be fetched every 15 minutes. + Add notes + Notes for the account + Allow to compress large photos into smaller sized photos with very less or negligible loss in quality of the image. + Allow compressing videos while maintaining their quality. + The app is compressing the media, it can be quite long… + Change app icon + Tap to change the app icon + Post + Visibility of the post + Tap here to add photos + Accepted Formats: jpeg, png, gif \n\nMax File Size: 15 MB \n\nAlbums can contain up to 4 photos or videos + Upload media + Add an optional caption + The app received a very long error message from the API %1$s + Message preview + Add mentions in each message + Fetching conversation + Order by + Title for the video + Join Peertube + I am at least 16 years old and agree to the %1$s of this instance + Links + Change the color of links (URLs, mentions, tags, etc.) in messages + Reblogs header + Change the color of display name at the top of messages + Change the color of the user name at the top of messages + Change the color of the header for reblogs + Posts + Background color of posts in timelines + Reset colors + Tap here to reset all your custom colors + Reset + Icons + Color of bottom icons in timelines + Pin this tag + Logo of the instance + Edit profile + Make an action + Translation + Image preview + Text color + Change the text color in pots + Apply changes + You need to restart the application to apply changes + Restart + Use a custom theme + Allow to override colors of the selected theme above + Theming + Store before + The theme was exported + The theme has been successfully exported in CSV + Apply the primary color to the status bar + Status bar color + Restore a default theme + Import a theme + Tap here to import a theme from a previous export + Export the theme + Tap here to export the current theme + An error occurred when selecting the theme file + Theme Picker + Select a pre-installed theme + Themes + Apply the primary color to the navigation bar + Navigation bar color + The underlying color of the app’s content. + Background color + Accents select parts of the UI. + Accent color + Displayed most frequently across your app. + Primary color + Export bookmarks to the instance + Import bookmarks from the instance + User count + Status count + Instance count + Blocked + End in %s + What\'s new in %s + You can follow my account for updates + This instance is not available on https://instances.social + Display full link + Share link + The URL has been copied to the clipboard + Open with another app + Check redirect + This URL does not redirect + %1$s \n\nredirects to\n\n %2$s + Change the user agent + Set a custom user agent or leave blank + Allows to customize the user agent used for api calls or with the built-in browser. + Remove UTM parameters + The app will automatically remove UTM parameters from URLs before visiting a link. + Trends + Trending now + %d people talking + Twitter accounts (via Nitter) + Twitter usernames space separated + Identity proofs + Verified identity + Verified by %1$s (%2$s) + Delete the notification + Display more options + It is a Pixelfed story + Upload a media, it will be automatically added to your Pixelfed story. + Media successfully added to your story! + Action disabled + Unfollow + Something went wrong, please check your download directory in settings. + Announcements + No announcements! + Add a reaction + Use your favourite browser inside the app. Uncheck this feature to open links externally. + Video cache in MB, zero means no cache. + Watermarks + Automatically add a watermark at the bottom of pictures. The text can be customized for each account. + No distributors found! + You need a distributor for receiving push notifications.\nYou will find more details at %1$s.\n\nYou can also disable push notifications in settings for ignoring that message. + Select a distributor + diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml new file mode 100644 index 000000000..ffe4039be --- /dev/null +++ b/app/src/main/res/values-it/strings.xml @@ -0,0 +1,1142 @@ + + + Apri il menù + Chiudi il menù + Info + Info sull\'istanza + Privacy + Cache + Disconnettiti + Accedi + + Chiudi + Si + No + Annulla + Scarica + Scarica %1$s + Media salvato + File: %1$s + Password + Email + Account + Toot + Etichette + Salva + Ripristina + Nessun risultato! + Istanza + Istanza: mastodon.social + Ora funziona con l\'account %1$s + Aggiungi un account + Il contenuto del toot è stato copiato negli appunti + L\'URL del toot è stato copiato negli appunti + Cambia + Seleziona un\'immagine… + Pulisci + Fotocamera + Elimina tutto + Traduci questo toot. + Pianifica + Dimensioni testo e icona + Cambia la dimensione attuale del testo: + Cambia la dimensione attuale delle icone: + Successivo + Precedente + Apri con + Conferma + Media + Condividi con + Condiviso via Fedilab + Risposte + Nome utente + Bozze + Preferiti + Nuovi follower + Menzioni + Ricondivisi + Mostra boosts + Mostra risposte + Apri nel browser + Traduci + Per favore, attendi alcuni secondi prima di effettuare questa azione. + + Home + Timeline locale + Timeline federata + Opzioni + Preferiti + Comunicazione + Utenti silenziati + Utenti bloccati + Notifiche + Richieste di follow + Impostazioni + Elimina un account + Eliminare l\'account %1$s dall\'applicazione? + Invia un\'email + Premi sul percorso per cambiarlo + Errore! + Toot programmati + Le informazioni qui sotto potrebbero riflettere in modo incompleto il profilo dell\'utente. + Inserisci emoji + L\'app non raccoglie emoji personalizzate per il momento. + Notifiche push + Sei sicuro di volerti disconnettere? + Sei sicuro di volerti disconnettere @%1$s@%2$s? + + Nessun toot da mostrare + Nessuna storia da mostrare + Storie + Ricondiviso da %1$s + Aggiungere questo toot ai tuoi preferiti? + Rimuovere questo toot dai tuoi preferiti? + Condividere questo toot? + Smettere di condividere questo toot? + Mettere questo toot in evidenza? + Rimuovere questo toot da quelli in evidenza? + Silenzia + Blocca + Segnala + Rimuovi + Copia + Condividi + Menziona + Silenzio temporizzato + Cancella & riscrivi + + Silenziare questo account? + Bloccare questo account? + Segnalare questo toot? + Bloccare questo dominio? + Smettere di silenziare questo account? + Sbloccare questo account? + + + Notifica + Silenzia + + + Rimuovere questo toot? + Cancellare & riscrivere questo toot? + + Segnalibri + Aggiungi ai segnalibri + Rimuovi segnalibro + Nessun segnalibro da visualizzare + Lo stato è stato aggiunto al segnalibro! + Lo stato è stato rimosso dal segnalibro! + + %d s + %d m + %d o + %d g + + %d secondo + %d secondi + + + %d minuto + %d minuti + + + %d ora + %d ore + + + %d giorno + %d giorni + + + Attenzione + Cosa ti passa per la mente? + TOOT! + QUEET! + cw + Scrivi un toot + Rispondi ad un toot + Scrivi un queet + Rispondi ad un queet + Seleziona un media + Si è verificato un errore durante la selezione del media! + Eliminare questo media? + Il tuo toot è vuoto! + Visibilità del toot + Visibilità predefinita dei toot: + Il toot è stato inviato! + Stai rispondendo a questo toot: + Contenuto sensibile? + + Pubblica nella timeline pubblica + Non pubblicare nella timeline pubblica + Pubblica solo verso i follower + Pubblica solo verso gli utenti menzionati + + Nessuna bozza! + Seleziona un toot + Seleziona un account + Seleziona alcuni account + Rimuovere bozza? + Premi il pulsante per visualizzare il toot originale + Descrivi per ipovedenti + + Nessuna descrizione disponibile! + + Versione %1$s + Sviluppatore: + Licenza: + GNU GPL V3 + Codice sorgente: + Traduzione dei toot: + Cerca istanze: + Designer icona: + + Conversazione + + Nessun account da visualizzare + Nessuna richiesta di follow + Toot \n %1$s + Seguendo \n %1$s + Follower \n %1$s + In evidenza \n %d + Autorizza + Rifiuta + + Nessun toot programmato da visualizzare! + Scrivi un toot e seleziona Programma dal menu in alto. + Eliminare toot programmato? + Media: %d + Il toot è stato programmato! + La data di programmazione deve essere successiva all\'ora corrente! + Il risparmio batteria è attivo! Potrebbe non funzionare correttamente. + + Il tempo di silenziamento deve essere maggiore di un minuto. + %1$s è stato silenziato fino al %2$s.\n Puoi smettere di silenziare questo account dalla sua pagina di profilo. + %1$s è silenziato fino al%2$s.\n Premi qui per smettere di silenziare l\'account. + + Nessuna notifica da visualizzare + ti ha menzionato + scrivi un nuovo messaggio + ha ricondiviso il tuo stato + ha messo tra i preferiti il tuo stato + ti segue + ha richiesto di seguirti + + e un\'altra notifica + e %d altre notifiche + + + %d mi piace + %d mi piace + + Eliminare una notifica? + Eliminare tutte le notifiche? + La notifica è stata eliminata! + Tutte le notifiche sono state eliminate! + + Seguendo + Follower + In evidenza + + Impossibile ottenere il client id! + Impossibile connettersi al dominio dell\'istanza! + Nessuna connessione ad internet! + L\'account è stato bloccato! + Questo account non è più bloccato! + L\'account è stato silenziato! + Questo account non è più silenziato! + L\'account è seguito! + L\'account non è più seguito! + Il toot è stato ricondiviso! + Il toot non è più ricondiviso! + Il toot è stato aggiunto ai tuoi preferiti! + Il toot è stato rimosso dai tuoi preferiti! + Il toot è stato segnalato! + Il toot è stato eliminato! + Il toot è stato messo in evidenza! + Il toot non è più in evidenza! + Oops! Si è verificato un errore! + Si è verificato un errore! L\'istanza non ha restituito un codice di autorizzazione! + Il dominio dell\'istanza non sembra essere valido! + Si è verificato un errore durante il passaggio tra gli account! + Si è verificato un errore durante la ricerca! + I dati del profilo sono stati salvati! + Nessuna azione può essere intrapresa + Il media è stato salvato! + Si è verificato un errore durante la traduzione! + Le traduzioni sono disabilitate nelle impostazioni + Bozza salvata! + Sei sicuro che questa istanza permetta questo numero di caratteri? Normalmente, questo valore è più vicino ai 500 caratteri. + La visibilità dei toot è stata cambiata per l\'account %1$s + + Numero di toot per caricamento + Sempre + WIFI + Chiedi + Carica i media + Carica le foto + Mostra di più… + Mostra meno… + Contenuto sensibile + Disabilita avatar GIF + Percorso: + Salva bozze automaticamente + Aggiungi URL dei media nei toot + Notifica quando qualcuno ti segue + Notifica quando qualcuno ricondivide al tuo stato + Notifica quando qualcuno mette tra i preferiti il tuo stato + Notifica quando qualcuno ti menziona + Notifica quando termina un sondaggio + Notifica per nuovi post + Mostra finestra di dialogo prima della ricondivisione + Mostra finestra di dialogo prima di aggiungere ai preferiti + Notifica solo in WIFI + Notifica? + Notifiche silenziose + Timeout visualizzazione NSFW (secondi, 0 significa disattivato) + Timeout descrizione media (secondi, 0 significa disattivato) + Modifica profilo + Condivisione personalizzata + L\'URL della tua condivisione personalizzata… + Biografia… + Blocca account + Salva modifiche + Seleziona una foto per l\'header + Adatta anteprima immagini + Automaticamente dividi toot superiori ai 500 caratteri in risposte + Hai raggiunto i 160 caratteri permessi! + Hai raggiunto i 30 caratteri permessi! + Tra le + e le + Il tempo deve essere maggiore di %1$s + Il tempo deve essere minore di %1$s + Tempo di inizio + Tempo di fine + Usa il browser incluso + Schede personalizzate + Abilita Javascript + Espandi automaticamente cw + Permetti cookie di terze-parti + La tua chiave API, puoi lasciarla vuota per Yandex + + Scuro + Chiaro + Nero + + Imposta colore LED: + + Blu + Ciano + Magenta + Verde + Rosso + Giallo + Bianco + + Segui + Sblocca + Silenzia + Non silenziare + Richiesta inviata + Ti segue + Cerca + Prima lettera in maiuscolo per le risposte + Ridimensiona foto + Rimpicciolisci video + + Notifiche push + Per favore, conferma le notifiche push che vuoi ricevere. + Puoi abilitare o disabilitar queste notifiche dopo nelle impostazioni (Scheda notifiche). + + + Pulisci cache + Ci sono %1$s di dati nella cache.\n\n Li vuoi eliminare? + Mb + La cache è stata pulita! %1$s sono stati liberati + + Titolo + Titolo… + Descrizione + Parole chiave + Parole chiave… + + Sincronizza + Filtro + I tuoi toot + Le tue notifiche + Pubblico + Non elencato + Privato + Diretto + Alcune parole chiave… + Mostra media + Mostra in evidenza + Nessuna corrispondenza trovata! + Backup dei toot per %1$s + %1$s nuovi toot sono stati importati + %1$s nuove notifiche sono state importate + + Date decrescenti + Date crescenti + + + No + Solo + Entrambi + + Nessun toot è stato trovato nel database. Per favore, utilizza il pulsante di sincronizzazione dal menu per recuperarli. + + Dati registrati + Sul dispositivo sono salvati sono le informazioni basilari degli account. + TQuesti sono strettamente confidenziali e possono essere utilizzati sono dall\'applicazione. + Eliminando l\'applicazione rimuoverà immediatamente questi dati.\n + ⚠ Login e password non vengono mai salvati. Vengono solo usati durante una autenticazione sicura (SSL) con un istanza. + + Permessi: + - ACCESS_NETWORK_STATE: Usato per determinare se il dispositivo è connesso ad una rete WIFI.\n + - INTERNET: Usato per le ricerche di un isatnza.\n + - WRITE_EXTERNAL_STORAGE: Usato per salvare i media o per spostare l\'app su una scheda SD.\n + - READ_EXTERNAL_STORAGE: Usato per aggiungere media ai toot.\n + - BOOT_COMPLETED: Usato per avviare il servizio di notifica.\n + - WAKE_LOCK: Usato durante il servizio di notifica. + + Permessi API: + - Read: Legge dati.\n + - Write: Posta gli stati e carica i media per gli stati.\n + - Follow: Seguire, non seguire, bloccare, sbloccare.\n\n + ⚠ Queste azioni sono utilizzare solo quando l\'utente le richiede. + + Tracciamento e librerie + L\'applicazione non utilizza tool di tracciamento (audience measurement, error reporting, ecc.) e non contiene pubblicità.\n\n + L\'utilizzo di librerie è minimizzato: \n + - Glide: Per gestire i media\n + - Android-Job: Per gestire i servizi\n + - PhotoView: Per gestire le immagini\n + + Traduzione del toot + L\'applicazione offre la possibilità di tradurre i toot utilizzando il dispositivo locale e le API di Yandex.\n + Yandex ha la propria privacy policy che può essere trovata qui: https://yandex.ru/legal/confidential/?lang=en + + Grazie anche a: + + Filtra tramite espressioni regolari + Cerca + Elimina + Recupera altri toot… + + Liste + Sei sicuro di voler eliminare definitivamente questa lista? + Non c\'è ancora niente in questa lista. Quando i membri di questa lista postano nuovi stati, questi appariranno qui. + Aggiungi alla lista + Aggiungi lista + Elimina lista + Modifica lista + Titolo della nuova lista + L\'account è stato aggiunto alla lista! + Non hai ancora nessuna lista! + + %1$s è stato spostato su %2$s + L\'autenticazione non funziona? + Qui ci sono alcuni controlli che possono aiutare:\n\n + - Controlla che non ci siano errori di trascrizione nel nome dell\'istanza\n\n + - Controlla che la tua istanza sia raggiungibile\n\n + - Se usi l\'autenticazione a due fattori (2FA), usa il link in basso (una volta che il nome dell\'istanza è stato inserito)\n\n + - Puoi anche usare questo link senza usare la 2FA\n\n + - Se ancora continua a non funzionare, apri un issue su Framagit all\'indirizzo https://framagit.org/tom79/fedilab/issues + + Il media è stato caricato. Premi qui per visualizzarlo. + Questa azione richiede molto tempo. Verrai avvisato quando sarà finita. + Ancora in corso, attendere prego… + Esporta stati + Stati esportati per %1$s + %1$s toot di %2$s sono stati esportati. + Qualcosa è andato storto esportando i dati per %1$s + Qualcosa è andato storto durante l\'esportazione dei dati! + Qualcosa è andato storto durante l\'importazione dei dati! + + Proxy + Abilitare il proxy? + Host + Porta + Login + Password + Aggiungi dettagli del toot quando condividi + Supporta l\'app su Liberapay + C\'è un errore nell\'espressione regolare! + Nessuna timeline trovata su questa istanza! + Eliminare questa istanza? + Traduci in + Segui istanza + Stai già seguendo questa istanza! + L\'istanza è seguita! + Collaborazioni + Informazioni + Nascondi boost da %s + Metti in evidenza sul profilo + Mostra boost da %s + Non mettere in evidenza sul tuo profilo + L\'account ora è in evidenza sul tuo profilo + L\'account ora non è più in evidenza sul tuo profilo + I boost ora sono visibili! + I boost ora sono nascosti! + Messaggi diretti + Filtri + Nessun filtro da visualizzare. Puoi crearne uno premendo il bottone \"+\". + Parola chiave o frase + La tua timeline + Timeline pubblica + Notifiche + Conversazioni + Il confronto sarà eseguito ignorando minuscole/maiuscole e i content warning di un toot + Ignorare invece di nascondere + I toot filtrati scompariranno in modo irreversibile, anche se il filtro verrà successivamente rimosso + Quando la parola chiave o la frase è solo alfanumerica, sarà applicato solo se corrisponde alll\'intera parola + Parola intera + Filtra contesti + Uno o più contesti dove applicare il filtro + Scade dopo + Elimina filtro? + Aggiorna filtro + Crea filtro + Chi seguire + Non c\'è nessun account elencato per il momento! + Segui + Seleziona tutti + Deseleziona tutti + %s è seguito! + Creazione della lista %s + Aggiunta degli account alla lista + Gli account sono stati aggiunti alla lista + Aggiunta degli account alla lista + Non hai ancora creato nessuna lista. Premi il pulsante \"+\" per aggiungerne una nuova. + Chi seguire + API Trunk + Il/gli account non può/possono essere seguito/i + Recupero dell\'account remoto + Espandi automaticamente i media nascosti + Una nuova persona ti segue + Un nuovo boost + Un nuovo mi piace + Una nuova menzione + Sondaggio terminato + Un nuovo toot + Backup dei toot + Nuovi post + Scarica i media + Cambia il suono di notifica + Seleziona tono + Abilita fascia oraria + Video di aiuto + Recupero della conversazione remota! + Nessun dominio bloccato! + Sblocca dominio + Sei sicuro di voler sbloccare %s? + Sei sicuro di voler bloccare %s? + Domini bloccati + Blocca dominio + Il dominio è bloccato + Il dominio non è più bloccato! + Recupero dello stato remoto + Commenta + Istanza Peertube + Lascia per primo un commento su questo video premendo il pulsante in alto a destra! + %s visualizzazioni + Durata: %s + Aggiungi un\'istanza + I commenti non sono abilitati per questo video! + Scegli una risoluzione + Preferiti di Peertube + Il video è stato aggiunto ai preferiti! + Il video è stato rimosso dai preferiti! + Non c\'è nessun video di Peertube nei tuoi preferiti! + Canale + Video + Canali + Utilizza le Emoji One + Informazioni + Mostra anteprime in tutti i toot + Nuovo designer UX/UI + Mostra le anteprime dei video + L\'ID dell\'account è stato copiato negli appunti! + Cambia lingua + Lingua predefinita + Tronca toot lunghi + Tronca toot più lunghi di \'x\' linee. Zero significa disabilitato. + Mostra di più + Mostra meno + Gestisci etichette + L\'etichetta esiste già! + L\'etichetta è stata salvata! + L\'etichetta è stata modificata! + L\'etichetta è stata eliminata! + Programma boost + Il boost è programmato! + Nessun boost programmato da mostrare! + Programma boost.]]> + Timeline Arte + Apri menu + Torna indietro + Logo dell\'applicazione + Immagine di profilo + Immagine di copertina + Contatta l\'amministratore dell\'istanza + Aggiungi nuovo + Logo di MastoHost + Selettore di emoji + Aggiorna + Espandi la conversazione + Rimuovi un account + Elimina il dominio bloccato + Selettore di emoji personalizzate + Riproduci video + Nuovo toot + Immagine della scheda + Nascondi media + Favicon + Media per l\'aggiunta di una descrizione + + Mai + 30 minuti + 1 ora + 6 ore + 12 ore + 1 giorno + 1 settimana + + In questo campo, devi scrivere il nome della tua istanza.\nPer esempio, se hai creato il tuo account su https://mastodon.social\nscrivi solo mastodon.social (senza https://)\n + Puoi iniziare a digitare le prime lettere ed i nomi verranno suggeriti.\n\n + ⚠ Il pulsante di accesso funzionerà solo se il nome dell\'istanza è valido e l\'istanza è online! + + Più informazioni + + Lingue + Solo media + Mostra NSFW + Traduzioni da Crowdin + Gestore di Crowdin + Traduzione dell\'applicazione + Info su Crowdin + Bot + Istanza Pixelfed + Istanza Mastodon + Qualsiasi tra queste + Tutte queste + Nessuna di queste + Qualsiasi tra queste parole (separate da spazi) + Tutte queste parole (separate da spazi) + Aggiungi alcune parole da filtrare (separate da spazio) + Rinomina colonna + Istanza Misskey + Nessuna app che supporta questo collegamento è installata sul tuo dispositivo. + Iscrizioni + Panoramica + Di tendenza + Aggiunti di recente + In locale + Carica + Rispondi + Elimina il commento + Sei sicuro di voler eliminare questo commento? + Video a schermo intero + Modalità per i video + Seleziona il file da caricare + I miei video + Titolo + Licenza + Categoria + Lingua + Questo video contiene contenuti espliciti o per maggiorenni + Abilita commenti al video + Aggiorna video + Descrizione + Il video è stato aggiornato! + Caricamento annullato! + Il video è stato caricato! + Caricamento, attendere prego… + Premi qui per modificare i dati del video. + Elimina video + Sei sicuro di voler eliminare questo video? + Mostrare video NSFW + Nessun video da mostrare! + Lascia un commento + Condividi + Scegli una modalità di pianificazione + Dal dispositivo + Dal server + Toot (Server) + Toot (Dispositivo) + Modifica + Visualizza nuovi toot sopra il pulsante \"Recupera altri toot\" + Timeline + Interfaccia + Contatti + %1$s ha commentato il tuo video %2$s]]> + %1$s sta seguendo il tuo canale %2$s]]> + %1$s sta seguendo il tuo account]]> + %1$s è stato pubblicato]]> + %1$s ha avuto successo]]> + %1$s non è riuscita]]> + %1$s ha pubblicato un nuovo video: %2$s]]> + %1$s è stato messo nella lista nera]]> + %1$s è stato rimosso dalla lista nera]]> + Esporta dati + Importa dati + Seleziona il file da importare + Si è verificato un errore durante la selezione del file di backup! + Aggiungi un commento pubblico + Invia commento + Non c\'è connessione ad Internet. Il tuo messaggio è stato salvato nelle bozze. + Testo normale + HTML + Markdown + Disconnetti l\'account + Tutto + Supporta l\'app + Open Collective permette ai gruppi di impostare velocemente una colletta per raccogliere fondi e gestirli in maniera trasparente. + Copia collegamento + Connettiti + Normale + Compatto + Terminale + Imposta modalità di visualizzazione + Regola il Provider di Sicurezza + Aggiorna domini traccianti + La base dati dei domini traccianti è stata aggiornata! + chiamate http bloccate dall\'applicazione + Lista di chiamate bloccate + Invia + La base di dati è stata esportata! + Hashtag in evidenza + Filtra la timeline con i tag + Nessun tag + Nascondi pulsante per l\'eliminazione delle notifiche nella relativa tab + Recupera metadati se l\'URL che condivide da altre applicazioni + + Sondaggio + Sondaggi + Crea un sondaggio + Scelta 1 + Scelta 2 + Scelta %d + Hai bisogno di almeno due scelte per il sondaggio! + Fatto + termina tra %s + Aggiorna sondaggio + Vota + Un sondaggio in cui hai votato è terminato + Un tuo sondaggio è terminato + Personalizza + Categorie + Fascia oraria + Avanzate + Mostra l\'icona \'nuovo\' sui toot non letti + Peertube + Sposta timeline + Nascondi timeline + Riordina timeline + Lista eliminata definitivamente + Istanza seguita rimossa + Etichetta fissata rimossa + Annulla + Devi mantenere almeno due schede visibili! + Riordina timeline + Le timeline principali possono solo essere nascoste! + BBCode + Segna sempre media come sensibili + Istanza GNU + Stato in cache + Inoltra le etichette nelle risposte + Premere a lungo per salvare media + Sfoca media con contenuti sensibili + Mostra le timeline in un elenco + Mostra timeline + Segna account bot nei toot + Gestisci etichette + Ricorda la posizione nella timeline principale + Cronologia + Playlist + Nome visualizzato + Non hai nessuna playlist. Premi l\'icona \"+\" per aggiungere una nuova playlist + È necessario fornire un nome da visualizzare! + Il canale è richiesto quando la playlist è pubblica. + Crea una playlist + Non c\'è ancora nulla in questa playlist. + ripeti + Galleria + Emoji + Adesivo + Gomma da cancellare + Testo + Filtro + Pennello + Sei sicuro di voler uscire senza salvare l\'immagine? + Scarta + Salvataggio… + Immagine salvata con successo! + Impossibile salvare l\'immagine + Opacità + Abilita editor foto + Aggiungi una voce al sondaggio + Rimuovi l\'ultima voce del sondaggio + Silenzia conversazione + Smetti di silenziare conversazione + La conversazione non è più silenziata! + La conversazione è silenziata + Apri le funzionalità dell\'applicazione + Silenzio temporizzato + Menziona l\'account + Aggiorna cache + Menziona lo stato + Novità + Generale + Locale + Arte + Giornalismo + Attivismo + Gioco + Tecnologia + Contenuto per adulti + Furry + Cibo + Icona dell\'istanza + Qualcosa è andato storto nel cercare istanze disponibili! + Unisciti a Mastodon + Scegli un\'istanza selezionando una categoria, poi premi il pulsante con la spunta. + Scegli un\'istanza premendo il pulsante di controllo. + %1$s utenti + Conferma password + Sono d\'accordo con %1$s e %2$s + regole del server + termini di servizio + Registrati + Questa istanza funziona con gli inviti. Il tuo account deve essere approvato manualmente da un amministratore prima di essere utilizzabile. + Per favore, completa tutti i campi! + Le password non corrispondono! + L\'indirizzo email non sembra essere valido! + Il nome utente sara unico su %1$s + Ti sarà inviata una conferma via email + Usa almeno 8 caratteri + La password deve contenere almeno 8 caratteri + Il nome utente può contenere solo lettere, numeri e trattini bassi + Account creato! + Il tuo account è stato creato!\n\n + Dovresti confermare la tua email entro le prossime 48 ore.\n\n + Ora puoi connetterti al tuo account scrivendo %1$s nel primo campo e premendo Connetti.\n\n + Importante: Se la tua istanza richiede l\'approvazione, riceverai una mail solo quando sarà approvato il tuo account! + + Salvare il messaggio in bozze? + Amministrazione + Segnalazioni + Nessuna segnalazione da mostrare! + Riconnetti l\'account + L\'applicazione non è riuscita ad accedere alle funzionalità di amministrazione. Potresti aver bisogno di riconnettere l\'account per avere le autorizzazioni corrette. + Non risolte + Da remoto + Attive + In attesa + Disabilitato + Silenziato + Sospeso + Permessi + Stato email + Stato accesso + Iscritto il + IP più recente + Avvisa + Disabilita + Silenzia + Notifica l\'utente via e-mail + Avviso personalizzato + Utente + Moderatore + Amministratore + Confermato + Non confermato + Stati segnalati + Account + Annulla silenzia + Annulla disabilita + Sospendi + Annulla sospendi + L\'account è silenziato! + L\'account non è più silenziato! + L\'account è sospeso! + L\'account non è più sospeso! + L\'account è disabilitato! + L\'account non è più disabilitato! + L\'account è stato avvisato! + Mostra il menu di amministratore + Mostra le funzionalità di amministratore negli stati + Consenti + L\'account è approvato! + L\'account è rifiutato! + Assegna a me + Non assegnare più a me + Segna come risolto + Segna come non risolto + Senza contenuto! + Mostra il pulsante delle funzionalità di Fedilab + L\'applicazione richiede l\'accesso al microfono + Messaggio vocale + Abilita risposta veloce + L\'account al quale stai rispondendo potrebbe non vedere il tuo messaggio! + Se disabilitato, l\'app caricherà sempre gli ultimi stati + Se disabilitato, i media con contenuti sensibili saranno nascosti con un pulsante + Salva media in dimensione originale tenendo premuto nelle anteprime + Aggiungi un pulsante con i 3 puntini in alto a destra per elencare tutte le etichette/istanze/liste + Durante l\'intervallo di tempo, l\'app invierà notifiche. Puoi invertire (es: silenziare) questo intervallo di tempo con il menù a destra. + Mostra un pulsante di Fedilab sotto l\'immagine del profilo. Questa è una scorciatoia per accedere alle funzionalità dell\'app. + Consenti di rispondere direttamente nelle timeline sotto gli stati + Le anteprime non saranno ritagliate nelle timeline + Consenti di riprodurre video incorporati direttamente nelle timeline + Consenti di invertire il verso di lettura degli stati che sono mostrati una volta premuto il pulsante \"Recupera altri toot\" + Questa opzione consente di supportare i recenti metodi di crittografia. É utile per vecchi dispositivi Android o se non riesci a connetterti alla tua istanza. + Esclusivamente per video da Peertube. Cambia modalità se non riesci a riprodurli. + Queste etichette ti consentono di filtrare stati dai profili. Dovrai usare il menu contestuale per visualizzarli. + Inserisci automaticamente un\'interruzione di linea dopo la citazione per mettere in maiuscolo la prima lettera + Consenti ai creatori di contenuti di condividere stati ai loro Feed RSS + Composizione + Massimo numero di tentativi per il caricamento dei media + Crea una nuova Cartella qui + Inserisci il nome della cartella + Per favore inserisci un nome valido per la cartella + Questa cartella esiste già.\n Per favore fornisci un altro nome per la cartella + Seleziona + Cartella Predefinita + Cartella + Crea cartella + Mostrare un messaggio a comparsa dopo che un\'azione è stata completata (boost, mi piace, ecc.)? + Le istanze silenziate sono state esportate! + Aggiungi un istanza + Esporta le istanze + Importa istanze + Segnalazioni di crash + Abilita segnalazioni di crash + Se abilitato, una segnalazione di crash sarà creata localmente e quindi sarai in grado di condividerla. + Fedilab ha smesso di funzionare :-( + Puoi inviarmi via email il rapporto sul problema. Mi aiuterà a risolverlo :-)\n\nPuoi aggiungere ulteriori commenti. Grazie! + Usa il wysiwyg + Quando abilitato, sarai in grado di formattare il tuo testo facilmente con strumenti. + Statistiche + Stati totali + Numero di boost + Numero di preferiti + Numero di menzioni + Numero di seguiti + Numero di sondaggi + Numero di risposte + Numero di stati + Stati + Visibilità + Numero con file media + Numero con media sensibili + Numero con CW + Data primo stato + Data ultimo stato + Data della prima notifica + Ultima data di notifica + Frequenza + %s stati al giorno + %s notifiche al giorno + Intervallo di tempo + Gruppi + Nessun gruppo! + Disabilita emoji animate personalizzate + Grafici + Mostra grafici + L\'applicazione sta raccogliendo i tuoi dati locali, attendi per favore... + Backup + Fai automaticamente il backup degli stati + Questa azione è per account. Lancerà un servizio che automaticamente salva i tuoi stati localmente nel database. Questo consentirà di avere statistiche e grafici + Fai automaticamente il backup delle notifiche + Questa opzione è per account. Verrà avviato un servizio che memorizza automaticamente le notifiche localmente nel database. Questo permette di ottenere statistiche e grafici + Segnala account + Invia un invito + La tua istanza non consente di registrare un nuovo account! + + %d voto + %d voti + + + %d votante + %d votanti + + + Scelta singola + Scelta multipla + + + 5 minuti + 30 minuti + 1 ora + 6 ore + 1 giorno + 3 giorni + 7 giorni + + + Webview + Stream diretto + + Per unirti alla mia istanza \"%1$s\", è possibile scaricare Fedilab:\n\nF-Droid: %2$s\n: %3$s\n\nQuindi apri il link qui sotto con Fedilab e crea il tuo account :)\n\n%4$s + + Il tuo sondaggio non può avere opzioni duplicate! + Per tutti gli account + Cache del database + Cancella la cache della timeline home + Cancella i tuoi stati memorizzati nella cache + Cancella i tuoi segnalibri + File nella cache + Notifiche totali + Nascondi voci di menu + Fedilab sta eseguendo notifiche in tempo reale + Per %1$s account con %2$s eventi + Notifiche in tempo reale per %1$s + Le notifiche in tempo reale saranno abilitate per questo account. + Cancella cache quando esci + La cache (media, messaggi memorizzati nella cache, dati del browser integrato) sarà automaticamente cancellata quando l\'applicazione verrà chiusa. + Vuoi smettere di seguire questo account? + Mostra finestra di conferma prima di smettere di seguire + Sostituisci Youtube con Invidio.us + Invidious è un\'interfaccia alternativa per YouTube + Inserisci il tuo host personalizzato o lascia vuoto per usare invidio.us + Sostituisci Twitter con Nitter + Nitter è un\'interfaccia alternativa e open source per Twitter, focalizzata sulla privacy. + Inserisci il tuo host personalizzato o lascia vuoto per usare nitter.net + Sostituisci Instagram con Bibliogram + Bibliogram è un\'interfaccia alternativa e open source per Instagram, focalizzata sulla privacy. + Inserisci il tuo host personalizzato o lascia vuoto per usare bibliogram.art + Sostituisci Reddit con Libreddit + Libreddit è un\'interfaccia open source alternativa a Reddit focalizzata sulla privacy. + Inserisci il tuo host personalizzato o lascia vuoto per usare libredd.it + Replace Medium links + Replace medium.com links with an open source alternative front-end focused on privacy. + Default: scribe.rip + Replace Wikipedia links + Replace Wikipedia link with an open source alternative front-end focused on privacy. + Default: wikiless.org + Nascondi barra di notifica di Fedilab + Per nascondere le notifiche rimanenti nella barra di stato, premi il pulsante con l\'icona di un occhio e deseleziona: \"Mostra nella barra di stato\" + Usa un sistema di notifiche push per ricevere notifiche in tempo reale. + Nessuna notifica in tempo reale + Notifiche in tempo reale + Le notifiche saranno recuperate ogni 15 minuti. + Aggiungi note + Note per l\'account + Permette di comprimere foto grandi in foto di dimensione ridotta con poca o trascurabile perdita in qualità dell\'immagine. + Permette di comprimere video mantenendo la loro qualità. + L\'app sta comprimendo il media, può volerci un po\'… + Cambia icona dell\'app + Premi per cambiare l\'icona dell\'app + Pubblica + Visibilità del post + Premi qui per aggiungere foto + Formati Accettati: jpeg, png, gif \n\nDimensione massima del file: 15 MB \n\nGli album possono contenere fino a 4 foto o video + Carica media + Aggiungi una didascalia opzionale + L\'app ha ricevuto un messaggio di errore molto lungo dall\'API %1$s + Anteprima messaggio + Aggiungi menzioni in ogni messaggio + Recupero della conversazione + Ordina per + Titolo per il video + Unisciti a Peertube + Ho almeno 16 anni e accetto le %1$s di questa istanza + Collegamenti + Cambiare il colore dei collegamenti (URL, menzioni, etichette, ecc.) nei messaggi + Intestazione dei reblog + Cambia il colore del nome visualizzato nella parte superiore dei messaggi + Cambia il colore del nome utente nella parte superiore dei messaggi + Cambia il colore dell\'intestazione per i reblog + Messaggi + Colore di sfondo dei messaggi nelle timeline + Reimposta colori + Premi qui per reimpostare tutti i tuoi colori personalizzati + Reimposta + Icone + Colore delle icone sul fondo nelle timeline + Metti etichetta in evidenza + Logo dell\'istanza + Modifica profilo + Scegli un\'azione + Traduzione + Anteprima immagine + Colore del testo + Cambia il colore del testo nei riquadri + Applica le modifiche + È necessario riavviare l\'applicazione per applicare le modifiche + Riavvia + Usa un tema personalizzato + Permetti di non tenere conto dei colori del tema selezionato sopra + Temi + Archivia prima + Il tema è stato esportato + Il tema è stato esportato correttamente in CSV + Applica il colore primario alla barra di stato + Colore della barra di stato + Ripristina un tema predefinito + Importa un tema + Premi qui per importare un tema da un\'esportazione precedente + Esporta il tema + Premi qui per esportare il tema corrente + Si è verificato un errore durante la selezione del file di tema + Selettore Tema + Seleziona un tema preinstallato + Temi + Applica il colore primario alla barra di navigazione + Colore della barra di navigazione + Colore sottostante al contenuto dell\'app. + Colore di sfondo + Colori in tinta per alcune parti dell\'interfaccia. + Colore in tinta + Visualizzati più di frequente nella tua applicazione. + Colore primario + Esporta segnalibri nell\'istanza + Importa segnalibri dall\'istanza + Numero di utenti + Numero di stati + Numero di istanze + Bloccati + Termina tra %s + Cosa c\'è di nuovo nella %s + Puoi seguire il mio account per gli aggiornamenti + Questa istanza non è disponibile su https://instances.social + Mostra collegamento completo + Condividi collegamento + L\'URL è stato copiato negli appunti + Apri con un\'altra app + Controlla reindirizzamento + Questo URL non reindirizza + %1$s \n\nreindirizza verso\n\n %2$s + Cambia l\'agente dell\'utente + Imposta un agente utente personalizzato o lascia vuoto + Permetti la personalizzazione dell\'agente utente utilizzata per le chiamate dell\'API o con il browser integrato. + Rimuovi parametri UTM + L\'app rimuoverà automaticamente i parametri UTM dagli URL prima di aprire un collegamento. + Tendenze + Di tendenza ora + %d persone ne parlano + Account Twitter (via Nitter) + Nomi utente Twitter separati da spazio + Prove d\'identità + Identità verificata + Verificata da %1$s (%2$s) + Elimina la notifica + Mostra più opzioni + È una storia Pixelfed + Carica un media, che verrà automaticamente aggiunto alla tua storia Pixelfed. + Media aggiunto correttamente alla tua storia! + Azione disabilitata + Smetti di seguire + Qualcosa è andato storto, per favore controlla la cartella di download nelle impostazioni. + Annunci + Nessun annuncio! + Aggiungi una reazione + Usa il tuo browser preferito all\'interno dell\'app. Deseleziona questa funzione per aprire i link esternamente. + Cache video in MB, zero significa nessuna cache. + Filigrane + Aggiungi automaticamente una filigrana in fondo alle immagini. Il testo può essere personalizzato per ogni account. + Nessun distributore trovato! + Hai bisogno di un distributore per ricevere notifiche push.\nTroverai maggiori dettagli a %1$s.\n\nPuoi anche disabilitare le notifiche push nelle impostazioni per ignorare quel messaggio. + Seleziona un distributore + diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml new file mode 100644 index 000000000..5f319ad80 --- /dev/null +++ b/app/src/main/res/values-ja/strings.xml @@ -0,0 +1,1125 @@ + + + メニューを開く + メニューを閉じる + このアプリついて + このインスタンスについて + プライバシー + キャッシュ + ログアウト + ログイン + + 閉じる + はい + いいえ + キャンセル + ダウンロード + ダウンロード: %1$s + メディアを保存しました + ファイル:%1$s + パスワード + メールアドレス + アカウント + トゥート + タグ + 保存 + 復元 + 検索結果はありません! + インスタンス + インスタンス:mastodon.social + アカウント %1$s に切り替えました + アカウントを追加 + トゥートの内容をクリップボードにコピーしました + トゥートのURLをクリップボードにコピーしました + 変更 + 画像を選択… + 削除 + カメラ + すべて削除 + このトゥートを翻訳します。 + 予約 + テキストとアイコンのサイズ + 現在のテキストサイズを変更する: + 現在のアイコンサイズを変更する: + 次へ + 前へ + 他のアプリで開く + 完了 + メディア + 共有 + Fedilabで共有 + 返信 + ユーザー名 + 下書き + お気に入り + 新しいフォロワー + メンション + ブースト + ブーストを表示 + 返信を表示 + ブラウザで開く + 翻訳 + この操作を行う前に数秒お待ちください。 + + ホーム + ローカルタイムライン + 連合タイムライン + オプション + お気に入り + コミュニケーション + ミュートしたユーザー + ブロックしたユーザー + 通知 + フォローリクエスト + 設定 + アカウントを削除する + アプリケーションからアカウント %1$s を削除しますか? + メールを送信 + パスをタップして変更する + 失敗! + トゥート予約 + 以下の情報は、ユーザーのプロフィールが完全には反映されていない場合があります。 + 絵文字を挿入 + アプリはしばらくの間カスタム絵文字を収集していません。 + Push notifications + 本当にログアウトしますか? + \@%1$s@%2$sからログアウトしますか? + + トゥートがありません + 表示するストーリーがありません + ストーリー + %1$s にブーストされました + このトゥートをお気に入りに追加しますか? + このトゥートをお気に入りから削除しますか? + このトゥートをブーストしますか? + このトゥートのブーストを取り消しますか? + このトゥートをピン留めしますか? + このトゥートのピン留めを解除しますか? + ミュート + ブロック + 報告 + 削除 + コピー + 共有 + メンション + 時限ミュート + 削除して書き直す + + このアカウントをミュートしますか? + このアカウントをブロックしますか? + このトゥートを報告しますか? + このドメインをブロックしますか? + このアカウントのミュートを解除しますか? + このアカウントのブロックを解除しますか? + + + 通知 + サイレント + + + このトゥートを削除しますか? + このトゥートを削除して書き直しますか? + + ブックマーク + ブックマークに追加する + ブックマークを削除 + ブックマークがありません + トゥートがブックマークに追加されました! + トゥートがブックマークから削除されました! + + %d 秒 + %d 分 + %d 時間 + %d 日 + + %d秒 + + + %d分 + + + %d時間 + + + %d日 + + + 警告 + 今なにしてる? + トゥート! + QUEET! + cw + トゥートを書く + トゥートに返信 + Queetを書く + Queetに返信 + メディアを選択 + メディアの選択中にエラーが発生しました。 + このメディアを削除しますか? + トゥートが空です! + トゥートの公開範囲 + トゥートの既定の公開範囲: + トゥートを送信しました + このトゥートに返信中: + メディアを閲覧注意にする + + 公開タイムラインに投稿 + 公開タイムラインに投稿しない + フォロワーへの投稿のみ + メンションしたユーザーにのみ投稿 + + 下書きはありません! + トゥートを選択 + アカウントを選択 + 複数のアカウントを選択 + 下書きを削除しますか? + ボタンをタップするとオリジナルのトゥートが表示されます + 視覚障害者のための説明 + + 説明はありません! + + リリース %1$s + 開発者: + ライセンス: + GNU GPL V3 + ソースコード: + トゥートの翻訳: + インスタンスを検索: + アイコンデザイナー: + + 会話 + + アカウントがありません + フォローリクエストはありません + トゥート \n %1$s + フォロー中 \n %1$s + フォロワー\n %1$s + ピン留め \n %d + 承認する + 拒否 + + 予約したトゥートはありません! + トゥートを書き、トップメニューから 予約 を選択します。 + トゥートの予約を削除しますか? + メディア:%d + トゥートを予約しました! + 予約日時は現在の時間より後である必要があります! + バッテリーセーバーが有効になっています! 期待どおりに動作しない可能性があります。 + + ミュートする時間(1分超)です。 + %1$s は %2$s までミュートされています。\n このアカウントのプロフィールページでミュートを解除することができます。 + %1$s は %2$s までミュートされます。\n ミュートを解除するにはここをタップします。 + + 通知はありません + にメンションされました + wrote a new message + がブーストしました + がお気に入りに登録しました + があなたをフォローしました + フォローリクエストが来ています + + 別の通知 + + + %d いいね + + 通知を削除しますか? + すべての通知を削除しますか? + 通知は削除されました! + すべての通知が削除されました! + + フォロー中 + フォロワー + ピン留め + + クライアントIDを取得できませんでした。 + インスタンスドメインに接続できません! + インターネット接続がありません! + アカウントをブロックしました! + このアカウントのブロックを解除しました! + アカウントはミュートされました。 + このアカウントのミュートを解除しました! + アカウントをフォローしました! + アカウントのフォローを解除しました! + トゥートをブーストしました! + トゥートのブーストを取り消しました! + トゥートをお気に入りに追加しました + トゥートをお気に入りから削除しました! + トゥートを報告しました! + トゥートを削除しました! + トゥートをピン留めしました! + トゥートのピン留めを解除しました! + エラーが発生しました! + エラーが発生しました! インスタンスは認証コードを返しませんでした! + インスタンスのドメインが有効ではないようです! + アカウントの切り替え中にエラーが発生しました! + 検索中にエラーが発生しました! + プロフィールデータを保存しました! + 何もすることはできません + メディアを保存しました! + 翻訳中にエラーが発生しました! + 翻訳は設定で無効化されています + 下書きを保存しました! + インスタンスはこの文字数を許可していますか? 通常、この値は500文字程度です。 + アカウント %1$s のトゥートの公開範囲が変更されました + + 一回あたりに読み込むトゥートの数 + 常に + Wi-Fi + 尋ねる + メディアを読み込む + 写真をロードする + さらに表示… + 一部のみ表示... + 閲覧注意 + GIFアバターを無効にする + パス: + 下書きを自動的に保存する + トゥートにメディアのURLを追加する + フォローされたときに通知 + トゥートがブーストされたときに通知する + トゥートがお気に入りに追加されたときに通知する + メンションを受け取ったときに通知する + 投票が終了したら通知する + Notify for new posts + ブースト前に確認ダイアログを表示する + お気に入りに追加する前に確認ダイアログを表示する + WIFI接続時のみ通知 + 通知する + サイレント通知 + NSFW表示のタイムアウト(単位は秒、0でオフ) + メディアの説明のタイムアウト(単位は秒、0でオフ) + プロフィールを編集 + カスタム共有 + カスタム共有URL… + バイオ… + アカウントをロック + 変更内容を保存 + ヘッダー画像を選択 + プレビュー画像をフィットさせる + 500文字を超える返信のトゥートを自動的に分割する + 160文字の制限に達しました! + 30文字の制限に達しました! + 期間: + から + 時間は %1$s より後でなければなりません + 時間は %1$s より前でなければなりません + 開始時刻 + 終了時刻 + アプリ内ブラウザを使用する + カスタムタブ + Javascript を有効にする + CWを自動的に展開する + サードパーティのCookieを許可する + あなたのAPIキーを入力するか、Yandex向けに空欄にすることもできます + + ダーク + ライト + + + LEDの色: + + + シアン + マゼンタ + + + + + + フォローする + ブロック解除 + ミュート + ミュート解除 + リクエスト送信 + あなたをフォロー + 検索 + 返信で最初の文字を大文字にする + 画像をリサイズ + 動画のリサイズ + + プッシュ通知 + 受信したいプッシュ通知を確認してください。 +後でこれらの通知を設定で有効または無効にすることができます(通知タブ)。 + + キャッシュの消去 + キャッシュに %1$s のデータがあります。\n\n削除しますか? + Mb + キャッシュが消去されました! %1$s が解放されました + + タイトル + タイトル… + 説明 + キーワード + キーワード… + + 同期 + フィルター + あなたのトゥート + あなたの通知 + 公開 + 未収載 + 非公開 + メンション + キーワード... + メディアを表示 + ピン留めトゥートを表示 + 一致するものは見つかりませんでした! + %1$s のトゥートをバックアップ + %1$s 件の新しいトゥートをインポートしました + %1$s件の新しい通知をインポートしました + + 日付の降順 + 日付の昇順 + + + しない + のみ + する + + トゥートがデータベースに見つかりませんでした。取得するには、メニューから同期ボタンを使用してください。 + + 記録されたデータ + アカウントの基本情報のみがデバイスに保存されます。 +         これらのデータは厳密に機密情報であり、アプリケーションによってのみ使用できます。 +         アプリケーションを削除すると、すぐにこれらのデータが削除されます。\ n +         ⚠ログインとパスワードは保存されません。 これらは、インスタンスとのセキュア認証(SSL)中にのみ使用されます。 + 権限: + - ACCESS_NETWORK_STATE :端末がWIFIネットワークに接続されているかどうかを検出するために使用します。\n +        - インターネット:インスタンスへのクエリに使用されます。\n +        - WRITE_EXTERNAL_STORAGE :メディアを保存したり、SDカードにアプリを移動したりするのに使用します。\n +        - READ_EXTERNAL_STORAGE :トゥートにメディアを追加するために使用します。\n +        - BOOT_COMPLETED :通知サービスを開始するために使用されます。\n +        - WAKE_LOCK :通知サービスで使用されます。 + API権限: + - 読み取り:データを読み取ります。\n +        - 書き込み:トゥートの投稿とメディアのアップロードを行います。\n +        - フォロー:フォロー、フォロー解除、ブロック、ブロック解除を行います。\n\n +        ⚠ これらの操作は、ユーザーが要求したときにのみ実行されます。 + トラッキングとライブラリ + このアプリケーションはトラッキングツールを使用しておらず(利用者監視、エラー報告など)、広告もありません。\n\n +        ライブラリの使用は最小限です:\n +        - Glide: メディアの管理\n +        - Android-Job: サービスの管理\n +        - PhotoView: 画像の管理\n + + トゥートの翻訳 + このアプリケーションは、デバイスのロケールとYandex APIを使用してトゥートを翻訳する機能を提供します。\n +        Yandexは適切なプライバシーポリシーを持っています: https://yandex.ru/legal/confidential/?lang=ja + 感謝: + 正規表現でフィルタする + 検索 + 削除 + さらにトゥートを取得する… + + リスト + このリストを完全に削除してもよろしいですか? + このリストにはまだ何もありません。 このリストのメンバーが新しいトゥートを投稿すると、ここに表示されます。 + リストに追加 + リストを追加 + リストを削除 + リストを編集 + 新しいリストのタイトル + アカウントをリストに追加しました! + リストはありません! + + %1$s は %2$s へ引越しました + 認証が機能していませんか? + 役立つかもしれない確認事項:\n\n + - インスタンス名の記述に間違いが無いかをチェック\n\n + - インスタンスがダウンしていないかをチェック\n\n + - もし2段階認証(2FA)を利用している場合は、下部のリンクを使用してください(先にインスタンス名を入力)\n\n + - このリンクは、2FAを利用していなくても使用することができます\n\n + - もしまだ解決しない場合、Framagit(https://framagit.org/tom79/fedilab/issues)にissueを投稿してください + + メディアが読み込まれました。ここをタップして表示します。 + この操作には時間がかかることがあります。完了すると通知されます。 + 実行中です。お待ちください... + トゥートをエクスポート + %1$s のトゥートをエクスポート + %2$s 件中 %1$s 件のトゥートをエクスポートしました。 + %1$s のデータをエクスポート中に問題が発生しました + データのエクスポート中に問題が発生しました! + データのインポート中に問題が発生しました! + + プロキシ + プロキシの有効化 + ホスト + ポート + ログイン + パスワード + 共有時にトゥートの詳細を付加する + Liberapayでアプリを支援 + 正規表現に誤りがあります! + このインスタンスにタイムラインが見つかりませんでした! + このインスタンスを削除しますか? + 次の言語に翻訳 + インスタンスをフォロー + 既にこのインスタンスをフォローしています! + インスタンスがフォローされました! + パートナーシップ + 情報 + %s からのブーストを非表示 + プロフィールで紹介 + %s からのブーストを表示 + プロフィールから外す + アカウントはプロフィールで紹介されています + プロフィールの紹介からこのアカウントを削除しました + ブーストを表示するようにしました! + ブーストを非表示にしました! + ダイレクトメッセージ + フィルター + フィルターはありません。 \"+\" ボタンをタップして作成できます。 + キーワードまたはフレーズ + ホームタイムライン + 公開タイムライン + 通知 + 会話 + テキストの大文字/小文字や、トゥートの閲覧注意に関係なくマッチングされます + 非表示ではなく除外 + フィルターが後で削除されても、除外されたトゥートは元に戻せなくなります + キーワードまたはフレーズが英数字のみの場合、単語全体と一致する場合のみ適用されるようになります + 単語全体 + フィルター対象 + フィルターを適用する1つ以上の対象 + 期限 + フィルターを削除しますか? + フィルターの更新 + フィルターの作成 + おすすめのアカウント + 現在表示できるアカウントはありません! + フォロー + 全て選択 + 全て選択解除 + %s はフォローされています! + リスト %s を作成 + アカウントをリストに追加 + アカウントがリストに追加されました + アカウントをリストに追加 + まだリストを作成していません。新規に追加するには \"+\" ボタンをタップします。 + おすすめのアカウント + Trunk API + アカウントをフォローできません + リモートアカウントを取得中です + 非表示のメディアを自動的に展開する + 新しいフォロー + 新しいブースト + 新しいお気に入り + 新しいメンション + 投票終了 + 新しいトゥート + トゥート バックアップ + New posts + メディアのダウンロード + 通知サウンドを変更 + 音の選択 + 時間帯管理を有効化 + 操作解説動画 + リモートスレッドの取得中です! + ブロックしたドメインはありません! + ドメインのブロックを解除 + %s のブロックを解除しますか? + %s をブロックしますか?\n\nそのドメインのコンテンツをパブリックタイムラインや通知で見ることができなくなります。また、そのドメイン上のユーザーからのフォローは解除されます。 + ブロックしたドメイン + ドメインをブロック + ドメインはブロックされました + ドメインのブロックを解除しました! + リモートステータスを取得中 + コメント + Peertubeインスタンス + 右上のボタンをタップして最初のコメントを投稿しましょう! + %s 回再生 + 長さ: %s + インスタンスを追加 + この動画はコメントが無効です! + 画質を選択 + Peertubeのお気に入り + この動画をブックマークに追加しました! + この動画をブックマークから削除しました! + お気に入りのPeertubeの動画はありません! + チャンネル + 動画 + チャンネル + Emoji Oneを使用する + 情報 + 全てのトゥートでプレビューを表示する + 新しいUX/UIデザイナー + 動画プレビューを表示する + アカウントIDをクリップボードにコピーしました! + 言語の変更 + 既定の言語 + 長いトゥートを折りたたむ + \'x\'行以上のトゥートを切り捨てます。0は無効です。 + もっと見る + 非表示 + タグ管理 + 入力したタグはすでに存在します! + タグを保存しました! + タグを変更しました! + タグを削除しました! + ブースト予約 + ブーストを予約しました! + 予約済みブーストはありません! + ブースト予約を選択してください。]]> + アートタイムライン + メニューを開く + 戻る + アプリケーションのロゴ + プロフィールの写真 + プロフィールのバナー + このインスタンスの運営者に連絡する + 新規追加 + MastoHostのロゴ + 絵文字ピッカー + 更新 + 会話を展開する + アカウントを削除 + ブロックしたドメインを削除する + カスタム絵文字ピッカー + 動画を再生 + 新しいトゥート + カードのイメージ + メディアを非表示 + ファビコン + メディアの説明を追加(視覚障害者向け) + + 無し + 30 分 + 1 時間 + 6 時間 + 12 時間 + 1 日 + 1 週間 + + このフィールドにはサーバーのホスト名を入力してください。\n例えば、https://mastodon.socialでアカウントを作成した場合、mastodon.socialとだけ入力してください。\n最初の文字を入力するとそれに続く名前がサジェストされます。 + + 詳しい情報 + + 言語 + メディアのみ表示 + NSFWを表示 + Crowdinの翻訳 + Crowdinのマネージャー + アプリケーションの翻訳 + Crowdin について + ボット + Pixelfedインスタンス + Mastodonインスタンス + いずれか含む + すべてを含む + すべて除外 + これらの単語を除く(スペースで区切り) + 含める単語(スペースで区切り) + フィルターする単語を追加(スペースで区切り) + カラムの名前を変更 + Misskeyインスタンス + このリンクを開けるアプリがインストールされていません。 + サブスクリプション + 概要 + トレンド + 最近追加された項目 + ローカル + アップロード + 返信 + コメントを削除 + このコメントを削除しますか? + 全画面動画 + 動画のモード + アップロードするファイルを選択する + 動画 + タイトル + ライセンス + カテゴリ + 言語 + この動画は成人向けや過激な内容を含みます + 動画のコメントを有効にする + 動画をアップロード + 説明 + 動画を更新しました! + アップロードを中止しました! + 動画をアップロードしました! + アップロード中です。しばらくお待ちください… + タップして動画情報を編集します。 + 動画を削除 + この動画を本当に削除しますか? + NSFW動画を表示 + 動画がありません! + コメントする + 共有 + 予約モードを選択 + この端末から + サーバーから + トゥート(サーバー) + トゥート(端末) + 変更 + 新しいトゥートを「もっと見る」ボタンの上に表示する + タイムライン + インターフェイス + 連絡先 + %1$sが%2$sにコメントしました]]> + %1$sが%2$sをフォローしました]]> + %1$sがあなたのアカウントをフォローしました]]> + %1$sを投稿しました]]> + %1$sをインポートしました]]> + %1$sのインポートに失敗しました]]> + %1$sが新しい動画を投稿しました: %2$s]]> + %1$sがブラックリストに追加されました]]> + %1$sがブラックリストから外されました]]> + データのエクスポート + データのインポート + インポートするファイルを選択 + バックアップファイルの選択中にエラーが発生しました! + パブリックコメントを追加 + コメントを送信 + インターネット接続がありません。メッセージは下書きに保存しました。 + プレーンテキスト + HTML + マークダウン + アカウントからログアウト + すべて + アプリを支援する + Open Collectiveを使用すると、グループは資金を調達し透明性をもって管理するための集団をすばやく立ち上げることができます。 + リンクをコピー + 接続 + 標準 + コンパクト + コンソール + 表示モードの設定 + セキュリティプロバイダーを使用する + トラッキングドメインの更新 + トラッキングデータベースが更新されました! + httpコールがアプリケーションによってブロックされました + ブロックされたコールのリスト + 送信 + データベースがエクスポートされました! + 注目のハッシュタグ + タグでタイムラインをフィルターする + タグなし + 通知タブの「消去」ボタンを非表示にする + URLの共有時に画像を添付する + + アンケート + アンケート + アンケートを作成 + 選択肢1 + 選択肢2 + 選択肢 %d + アンケートには最低でも選択肢が2つ必要です! + 完了 + %sで締め切り + アンケートを更新 + 投票 + 投票したアンケートが締め切られました + トゥートしたアンケートが締め切られました + カスタマイズ + カテゴリ + 時間帯管理 + 上級者向け + 未読のトゥートに「new」バッジを表示する + Peertube + タイムラインの移動 + タイムラインを非表示にする + タイムラインの並び替え + 削除されたリスト + フォローされたインスタンスの削除 + ピン留めされたタグの削除 + 元に戻す + 最低でも2つタブを表示する必要があります! + タイムラインの並び替え + メインタイムラインは非表示にしかできません! + BBコード + メディアを常に閲覧注意に設定する + GNUインスタンス + キャッシュされたステータス + 返信でタグを先頭に付加する + 長押しでメディアを保存する + 閲覧注意のメディアをぼかす + リストにタイムラインを表示する + タイムラインを表示 + botアカウントによるトゥートに印をつける + タグ管理 + ホームタイムラインの位置を記憶する + 履歴 + プレイリスト + 表示名 + プレイリストがありません。プレイリストを作成するには\"+\"アイコンをタップします + 表示名を入力する必要があります! + プレイリストがパブリックのときにはチャンネルが必要です。 + プレイリストを作成 + プレイリストにはまだ何もありません。 + 元に戻す + ギャラリー + 絵文字 + ステッカー + 消しゴム + テキスト + フィルター + ブラシ + 画像を保存せずに閉じてよろしいですか? + 破棄 + 保存中... + 画像が正常に保存されました! + 画像を保存できませんでした + 透明度 + 画像エディターを有効化 + アンケートの項目を追加 + 最後のアンケートの項目を削除 + 会話をミュート + 会話のミュートを解除 + 会話のミュートを解除しました! + 会話をミュートしました + アプリの機能を開く + 時限ミュート + アカウントをメンション + キャッシュをリフレッシュ + ステータスをメンション + ニュース + 全般 + 地域 + アート + 報道 + 行動主義 + ゲームプレイ + テクノロジー + アダルトコンテンツ + ふわふわ + 食べ物 + インスタンスのロゴ + 有効なインスタンスのチェック中に問題が発生しました! + Mastodonに参加 + カテゴリーを選択してインスタンスを選択し、チェックボタンをタップしてください。 + チェックボタンをタップしてインスタンスを選択します。 + %1$s人のユーザー + パスワードの確認 + %1$sと%2$sに同意する + サーバーのルール + 利用規約 + 新規登録 + このインスタンスは招待制です。アカウントが利用可能になるには、管理者から手動で承認を受ける必要があります。 + 全ての欄を記入してください! + パスワードが一致しません! + このメールアドレスは無効です! + あなたのユーザー名は%1$s上に固有です + 確認メールを送信しました + 少なくとも8文字は入力してください + パスワードは8文字以上必要です + ユーザー名にはアルファベット、数字、アンダーバーのみ含めることができます + アカウントを作成しました! + アカウントを作成しました!\n\n + 48時間以内にメールアドレスを確認してください。\n\n + 最初の欄に%1$sと記入し、接続をタップするとアカウントに繋がります。\n\n + 重要:インスタンスが認証を要求する場合、一度確認のメールが送信されます! + + メッセージを下書きに保存しますか? + 管理 + 報告 + 通報はありません! + アカウントに再接続 + 管理者向け機能へのアクヘスに失敗しました。正常な動作のためにはアカウントに再接続する必要があります。 + 未解決 + リモート + アクティブ + 保留 + 無効 + サイレンス済み + 保留中 + 権限 + メールアドレスの状態 + ログイン状態 + 参加済み + 直近のIP + 警告 + 無効 + サイレント + メールでユーザーに通知 + カスタム警告文 + ユーザー + モデレーター + 管理者 + 確認済み + 未確認 + 通報されたトゥート + アカウント + サイレンスから戻す + 無効化を解除する + 停止 + 再開 + アカウントはサイレンスされています! + アカウントのサイレンスは解除されました! + アカウントは停止しています! + アカウントを再開させました! + アカウントは無効化されています! + アカウントが有効になりました! + このアカウントは警告を受けています! + 管理者メニューを表示する + トゥートに管理者機能を表示する + 許可 + アカウントが承認されました! + アカウントが拒否されました! + 担当になる + 担当を外す + 解決済みとしてマーク + 未解決として再び開く + コンテンツが空です! + Fedilab機能ボタンを表示する + このアプリは音声の録音へのアクセス許可が必要です + ボイスメッセージ + クイック返信を有効化する + あなたが返信しているアカウントにはあなたのメッセージが表示されていないかもしれません! + このオプションが無効の場合、常に最新のトゥートを読み込みます + 無効にすると、閲覧注意のメディアは非表示になり、ボタンだけが表示されます + プレビューで長押ししてメディアをオリジナルのサイズで保存します + 右上にすべてのタグ、インスタンス、リストを表示する楕円のボタンを追加します + 指定された時間帯に、アプリは通知を送信します。右のスピンボタンを押すと、設定を反転することができます。(指定された時間に非通知) + プロフィール画像の下にFedilabボタンを表示します。このボタンはアプリ内機能へのショートカットです。 + トゥートの下のタイムラインから直接返信できるようにします。 + タイムライン上のプレビューは切り取られません + タイムラインで埋め込み動画を直接再生できるようになります + もっと見るボタンを押して取得したトゥートを古いほうから読めるようになります + このオプションは最新の暗号スイートをサポートします。これは古いAndroid デバイスやインスタンスに接続できない場合に有効です。 + Peertubeの動画専用です。動画が再生できない場合にモードを切り替えてください。 + これらのタグはプロフィールからトゥートを取り除くことがあります。取り除かれたトゥートを表示するにはコンテキストメニューを使用する必要があります。 + 最初の文字を大文字にするために、メンションの後に自動的に改行を挿入します + コンテンツの作成者がRSSフィードにトゥートを共有できるようになります + トゥートの作成 + メディアのアップロード時の最大試行回数 + ここに新規フォルダーを作成 + フォルダー名を入力 + 有効なフォルダー名を入力してください + 同じ名前のフォルダーが既に存在しています。\n別の名前を付けてください + 選択 + 既定のディレクトリ + フォルダー + フォルダーを作成 + アクション(ブースト、お気に入りなど)が完了したらトースト通知を表示する + ミュートしたインスタンスをエクスポートしました! + インスタンスを追加 + インスタンスをエクスポート + インスタンスをインポート + クラッシュレポート + クラッシュレポートを有効にする + 有効にすると、ローカルでクラッシュレポートが作成され、共有できるようになります。 + Fedilabは停止しました :( + メールでクラッシュレポートを送ることができます。これは、問題の修正に役立ちます :)\n\n追加のコンテンツを付加することもできます。ありがとうございます! + WYSIWYGを使用する + 有効にすると、テキストの書式をツールで簡単に整形できるようになります。 + 統計情報 + 合計投稿数 + ブースト数 + お気に入りの数 + メンションしたユーザーの数 + フォローユーザーの数 + アンケートの数 + 返信数 + 投稿数 + 投稿 + 公開範囲 + メディア付き投稿数 + 閲覧注意メディア付き投稿数 + 警告文付き投稿数 + 最初の投稿日 + 最後の投稿日 + 最初の通知 + 前回の通知 + 投稿頻度 + %s トゥート/日 + 一日当たり%s件の通知 + 対象期間 + グループ + グループがありません! + 動くカスタム絵文字を無効にする + グラフ + グラフを表示する + データを収集しています。しばらくお待ちください。 + バックアップ + トゥートを自動でバックアップ + これはアカウントごとの設定です。トゥートをこの端末のデータベースに自動的に保存するサービスを起動します。これを有効にすると、統計とグラフを取得できるようになります。 + 自動バックアップの通知 + これはアカウントごとの設定です。通知をこの端末のデータベースに自動的に保存するためのサービスを起動します。これを有効にすると、統計とグラフを取得できるようになります。 + アカウントを報告 + 招待を送信 + このインスタンスは新規登録の受付を停止しています! + + %d票 + + + %d票 + + + 単一の選択 + 複数の選択 + + + 5分 + 30分 + 1時間 + 6時間 + 1日 + 3日 + 7日 + + + WebView + ダイレクトストリーム + + Fedilabをダウンロードしてインスタンス「%1$s」に参加できます:\n\nF-Droid: %2$s\nGoogle: %3$s\n\nダウンロードしたら、下のリンクをタップしてFedilabを開いて、アカウントを作成しましょう ^_^\n\n%4$s + アンケートに重複した選択肢を作ることはできません! + すべてのアカウントで実行 + データベースキャッシュ + キャッシュしたホームタイムラインを削除 + キャッシュしたトゥートを削除 + ブックマークを削除 + キャッシュ内のファイル + 全ての通知 + メニューの項目を非表示にする + Fedilabのライブ通知が動作中です + %1$s個のアカウントに%2$s個のイベントがあります + %1$s のライブ通知 + このアカウントではライブ通知が有効です。 + 終了時にキャッシュを消去する + アプリケーションの終了時にキャッシュ(メディア、キャッシュされたメッセージ、ビルトインブラウザのデータ)が自動的に消去されます。 + このアカウントのフォローを解除しますか? + フォローを解除する前に確認を表示する + YouTubeをInvidio.usに置き換える + InvidiousはYouTubeの代替フロントエンドです + カスタムホストを入力してください。空欄にするとinvidio.usが使用されます。 + TwitterをNitterに置き換える + Nitterはプライバシーに重点を置いたオープンソースのTwitter代替フロントエンドです。 + カスタムホストを入力してください。空欄にするとnitter.netが使用されます。 + InstagramをBibliogramに置き換える + Bibliogramはプライバシーを重視したオープンソースのInstagramの代替フロントエンドです。 + カスタムホストを入力してください。空欄にするとbibliogram.artが使用されます。 + Replace Reddit with Libreddit + Libreddit is an open source alternative Reddit front-end focused on privacy. + Enter your custom host or leave blank for using libredd.it + Replace Medium links + Replace medium.com links with an open source alternative front-end focused on privacy. + Default: scribe.rip + Replace Wikipedia links + Replace Wikipedia link with an open source alternative front-end focused on privacy. + Default: wikiless.org + 通知バーからFedilabを非表示にする + ステータスバー上の通知を非表示にするには、目のアイコンボタンをタップして「ステータスバーに表示する」のチェックを外します + Use a push notifications system for getting notifications in real time. + ライブ通知なし + Live notifications + 15分ごとに通知を取得します。 + メモを追加 + このアカウントのメモ + 大きな画像を非常に少ない無視できる程度の損失で小さい画像に圧縮できます。 + 品質を維持しつつ動画を圧縮できます。 + メディアを圧縮しています、これには時間がかかることがあります… + アプリアイコンを変更する + タップしてアプリアイコンを変更する + 投稿 + 投稿の公開範囲 + ここをタップして写真を追加する + 利用可能なフォーマット: jpeg, png, gif \n\n最大ファイルサイズ: 15MB \n\nアルバムには最大で4個の写真または動画を含めることができます + メディアをアップロード + オプションの説明を追加する + このアプリは%1$sAPIから非常に長いエラーメッセージを受け取りました + メッセージプレビュー + それぞれのメッセージにメンションを追加 + 会話を取得 + 並び順 + 動画のタイトル + Peertubeに参加 + 私は16歳以上で、インスタンスの %1$s に同意します。 + リンク + メッセージ内のリンク(URL、メンション、タグなど)の色を変更します + ブーストヘッダー + メッセージ一番上の表示名の色を変更します + メッセージ一番上のユーザー名の色を変更します + ブーストのヘッダーの色を変更します + 投稿 + タイムライン上の投稿の背景色 + 色をリセット + ここをタップしてすべての色のカスタマイズをリセットします + リセット + アイコン + 投稿下部にあるアイコンの色 + このタグをピン留め + インスタンスのロゴ + プロフィールを編集 + アクションを作成 + 翻訳 + 画像のプレビュー + テキストの色 + 投稿内のテキストの色を変更します + 変更を適用 + 変更を反映するにはアプリケーションを再起動する必要があります。 + 再起動 + カスタムテーマの使用 + 上で選択したテーマの色の上書きを許可します + テーマ設定 + 先に保存 + テーマをエクスポートしました + テーマをCSVファイルにエクスポートしました + プライマリカラーをステータスバーに使用する + ステータスバーの色 + デフォルトテーマを復元 + テーマのインポート + ここをタップして以前エクスポートしたテーマをインポートします + テーマのエクスポート + ここをタップして現在のテーマをエクスポートします + テーマファイルの選択中にエラーが発生しました + テーマ選択 + 内蔵のテーマを選択 + テーマ + ナビゲーションバーにプライマリカラーを適用します + ナビゲーションバーの色 + アプリ内コンテンツの背景色 + 背景色 + UIのアクセント部分の色を選択します。 + アクセントカラー + アプリ内で最も使用される色です。 + プライマリカラー + そのインスタンスにブックマークをエクスポート + このインスタンスからブックマークをインポート + ユーザー数 + トゥート数 + インスタンス数 + ブロック済み + %sで終了 + %sの新機能 + 私のアカウントをフォローすると新機能をチェックできます + このインスタンスは https://instances.social 上で利用できません + リンクを省略せずに表示 + リンクを共有 + URLをクリップボードにコピーしました + 別のアプリで開く + リダイレクト先を確認する + このURLはリダイレクトではありません + %1$s\n\nは\n\n%2$sにリダイレクトしています + ユーザエージェントを変更 + カスタムのユーザーエージェントを設定するか、空欄にしておいてください + APIの呼び出しやアプリ内ブラウザで使用するユーザーエージェントをカスタマイズできます + UTMパラメータを削除 + リンクを開く前に自動的にURLからUTMパラメータを取り除きます。 + トレンド + トレンド + %d人が話題にしています + Twitterアカウント(via Nitter) + Twitterのユーザー名をスペースで区切って入力 + 身分証明 + 認証済み + %1$s(%2$s)によって認証されました + 通知を消去 + さらに多くのオプションを表示 + Pixelfedのストーリーです + メディアをアップロードすると、自動的にあなたのPixelfedストーリーに追加されます。 + メディアをあなたのストーリーにアップロードしました! + アクションを無効化する + フォロー解除 + エラーが発生しました、設定からダウンロードディレクトリを確認してください。 + お知らせ + お知らせはありません! + リアクションを追加 + アプリ内でお気に入りのブラウザを使用します。リンクを外部アプリで開きたい場合はこのチェックを外してください。 + MB単位での動画キャッシュ、0でキャッシュしません。 + ウォーターマーク + 画像の下部に自動的にウォーターマークを追加します。テキストはアカウントごとにカスタマイズできます。 + No distributors found! + You need a distributor for receiving push notifications.\nYou will find more details at %1$s.\n\nYou can also disable push notifications in settings for ignoring that message. + Select a distributor + diff --git a/app/src/main/res/values-kab/strings.xml b/app/src/main/res/values-kab/strings.xml new file mode 100644 index 000000000..8cbd17db8 --- /dev/null +++ b/app/src/main/res/values-kab/strings.xml @@ -0,0 +1,1142 @@ + + + Ldi umuɣ + Mdel umuγ + Γef + Γef uqeddac + Tabaḍnit + Tazarkatut + Ffeγ + Qqen + + Mdel + Ih + Ala + Sefsex + Sider + Asider n %1$s + Amidya ittwasekles + Afaylu: %1$s + Awal uffir + Imayl + Imiḍan + Tijewwiqin + Tibzimin + Aḥrez + Err-d + Ulac igemmaḍ! + Aqeddac + Aqeddac: mastodon.social + Iteddu tura akked s umiḍan %1$s + Rnu amiḍan + The content of the toot has been copied to the clipboard + The URL of the toot has been copied to the clipboard + Beddel + Fren tugna… + Sfeḍ + Takamiṛat + Kkes kullec + Suqqel tajewwiqt-a + Sγiwes + Tuɣzi n tignit d uḍris + Snifel tuɣzi n tamirant n uḍris: + Snifel tuɣzi n tamirant n tignit: + Γer sdat + Γer deffir + Ldi s + Seγbel + Amidya + Bḍu d + Bḍu s Fedilab + Tiririt + Isem n useqdac + Irewwayen + Ismenyifen + Imeḍfaṛen imaynuten + Ibdaren + Boosts + Show boosts + Ssekned tiririt + Ldi deg iminig + Suqel + Ttxil, rǧu kra n tesenatin uqbel ad txedmeḍ tigawt-a. + + Agegdan + Tasuddemt tadigant n yisallen + Tasuddemt tafidiṛalit n yisallen + Tinefrunin + Ismenyifen + Taɣwalt + Imiḍanen yettwasgugmen + Imseqdacen yettusḥebsen + Ilγuyen + Isuturen n teḍfeṛt + Iγewwaṛen + Kkes amiḍan + Remove the account %1$s from the application? + Azen imayl + Tap on the path to change it + Icceḍ! + Scheduled toots + Information below may reflect the user\'s profile incompletely. + Rnu imuji + The app did not collect custom emojis for the moment. + Push notifications + Tebɣiḍ s tidet ad teffɣeḍ? + Are you sure you want to logout @%1$s@%2$s? + + Ulac ituten ara d-nesken + Ulac tiqsiḍin ara d-nesken + Tiqsiḍin + Ittwaru sɣur %1$s + Add this toot to your favourites? + Remove this toot from your favourites? + Boost this toot? + Unboost this toot? + Pin this toot? + Unpin this toot? + Susem + Seḥbes + Cetki + Kkes + Nγel + Bḍu + Bder + Timed mute + Delete & re-draft + + Susem amiḍan-a? + Block this account? + Report this toot? + Block this domain? + Unmute this account? + Eks asewḥel i umiḍan ayi? + + + Selɣu + Asusam + + + Delete this toot? + Delete & re-draft this toot? + + Ticraḍ + Rnu ar ticraḍ + Kkes tacreḍt + No bookmarks to display + Status has been added to bookmarks! + Status was removed from bookmarks! + + %d tsn + %d tsd + %d sr + %d ass + + %d n tasint + %d n tasinin + + + %d n tesdat + %d n tesdatin + + + %d n usrag + %d n isragen + + + %d day + %d n wussan + + + Acaγli + D acu i tettxammimeḍ? + JEWWEQ! + QUEET! + cw + Aru tajewwiqt + Err i tjewwiqt + Write a queet + Reply to a queet + Fren allal n teγwalt + An error occurred while selecting the media! + Remove this media? + Your toot is empty! + Visibility of the toot + Visibility of the toots by default: + The toot has been sent! + You are replying to this toot: + Sensitive content? + + Bḍu deg tsuddemt tazayezt + Do not post to public timelines + Azen iy imeḍfaṛen kan + Post to mentioned users only + + Wlac irewwayen! + Fren tijewwiqt + Fren amiḍan + Select some accounts + Kkes arewway? + Tap on the button to display the original toot + Describe for the visually impaired + + Ulac aglam! + + Lqem %1$s + Aneflay: + Turagt: + GNU GPL V3 + Tangalt taγbalut: + Tasuqilt n tijewwiqin: + Nadi tummant: + Icon designer: + + Asqerdec + + Ulac kra n umiḍan ara d-nesken + Ulac isuturen n uneḍfar + Tijewwiqin \n %1$s + Yeṭafaṛ \n %1$s + Imeḍfaṛen \n %1$s + Pinned \n %d + Sireg + Agi + + No scheduled toots to display! + Write a toot and then choose Schedule from the top menu. + Delete scheduled toot? + Allal n teγwalt: %d + The toot has been scheduled! + The scheduled date must be greater than the current hour! + Battery saver is enabled! It might not work as expected. + + The time for muting should be greater than one minute. + %1$s has been muted until %2$s.\n You can unmute this account from their profile page. + %1$s is muted until %2$s.\n Tap here to unmute the account. + + No notification to display + yuder-ik id + wrote a new message + boosted your status + favourited your status + i ḍferek id + asked to follow you + + and another notification + and %d other notifications + + + %d like + %d likes + + Delete a notification? + Delete all notifications? + The notification has been deleted! + All notifications have been deleted! + + Ittafar + Imeḍfaṛen + Inta + + Unable to get client id! + Unable to connect to instance domain! + Ulac tuqqna γer Internet! + The account was blocked! + The account is no longer blocked! + The account was muted! + The account is no longer muted! + The account was followed! + The account is no longer followed! + The toot was boosted! + The toot is no longer boosted! + The toot was added to your favourites! + The toot was removed from your favourites! + The toot was reported! + The toot was deleted! + The toot was pinned! + The toot was unpinned! + Oops ! An error occurred! + An error occurred! The instance did not return an authorisation code! + The instance domain does not seem to be valid! + An error occurred while switching between accounts! + An error occurred while searching! + The profile data have been saved! + No action can be taken + The media has been saved! + An error occurred while translating! + Translations are disabled in settings + Draft saved! + Are you sure this instance allows this number of characters? Usually, this value is close to 500 characters. + Visibility of the toots has been changed for the account %1$s + + Number of toots per load + Yal tikelt + WIFI + Suter + Salid allal n teγwalt + Sali-d tugniwin + Sken ugar… + Sken kra… + Sensitive content + Disable GIF avatars + Abrid: + Save drafts automatically + Add URL of media in toots + Notify when someone follows you + Notify when someone boosts your status + Notify when someone favourites your status + Notify when someone mentions you + Notify when a poll ended + Notify for new posts + Show confirmation dialog before boosting + Show confirmation dialog before adding to favourites + Notify in WIFI only + Notify? + Silent Notifications + NSFW view timeout (seconds, 0 means off) + Media Description timeout (seconds, 0 means off) + Zreg alegdis + Custom sharing + Your custom sharing URL… + Bio… + Sewḥel amiḍan + Sekles ibeddilen + Choose a header picture + Fit preview images + Automatically split toots in replies when chars are over: + You have reached the 160 characters allowed! + You have reached the 30 characters allowed! + Ger + d + The time must be greater than %1$s + The time must be lower than %1$s + Akud n tazwara + Azemz n tagara + Use the built-in browser + Iccaren udmawanen + Enable Javascript + Automatically expand cw + Allow third-party cookies + Your API key, you can leave blank for Yandex + + Tebrek + Aceɛlal + Aberkan + + Set LED colour: + + Azegzaw + Cyan + Axuxi + Adal + Azeggaγ + Awraγ + Amellal + + Ḍfeṛ + Serreḥ + Susem + Unmute + Request sent + Yeṭafar-ik id + Nadi + First letter in capital for replies + Resize pictures + Resize videos + + Push notifications + Please, confirm push notifications that you want to receive. + You can enable or disable these notifications later in settings (Notifications tab). + + + Sfeḍ tuffirt + There are %1$s of data in cache.\n\nWould you like to delete them? + Mb + Cache was cleared! %1$s were released + + Azwel + Azwel… + Aglam + Awalen n tsaruţ + Awalen n tsaruţ… + + Mtawi + Sizdeg + Tijewwiqin-ik·im + Your notifications + Azayez + Unlisted + Uslig + Direct + Some keywords… + Sken all n teγwalt + Sseken-ed wid yentan + No matching result found! + Backup toots for %1$s + %1$s new toots have been imported + %1$s new notifications have been imported + + Dates descending + Dates ascending + + + Ala + Only + I sin + + No toots were found in database. Please, use the synchronize button from the menu to retrieve them. + + Recorded data + Only basic information from accounts are stored on the device. + These data are strictly confidential and can only be used by the application. + Deleting the application immediately removes these data.\n + ⚠ Login and passwords are never stored. They are only used during a secure authentication (SSL) with an instance. + + Issirgen: + - ACCESS_NETWORK_STATE: Used to detect if the device is connected to a WIFI network.\n + - INTERNET: Used for queries to an instance.\n + - WRITE_EXTERNAL_STORAGE: Used to store media or to move the app on a SD card.\n + - READ_EXTERNAL_STORAGE: Used to add media to toots.\n + - BOOT_COMPLETED: Used to start the notification service.\n + - WAKE_LOCK: Used during the notification service. + + API permissions: + - Read: Read data.\n + - Write: Post statuses and upload media for statuses.\n + - Follow: Follow, unfollow, block, unblock.\n\n + ⚠ These actions are carried out only when user requests them. + + Tracking and Libraries + The application does not use tracking tools (audience measurement, error reporting, etc.) and does not contain any advertising.\n\n + The use of libraries is minimized: \n + - Glide: To manage media\n + - Android-Job: To manage services\n + - PhotoView: To manage images\n + + Translation of toots + The application offers the ability to translate toots using the locale of the device and the Yandex API.\n + Yandex has its proper privacy-policy which can be found here: https://yandex.ru/legal/confidential/?lang=en + + Tanmirt i: + + Filter out by regular expressions + Nadi + Kkes + Fetch more toots… + + Tibdarin + Are you sure you want to permanently delete this list? + There is nothing in this list yet. When members of this list post new statuses, they will appear here. + Rnu γer tabdart + Rnu tabdart + Kkes tabdart + Ẓreg umuɣ + Azwel amaynut n wumuɣ + The account was added to the list! + You don\'t have any lists yet! + + %1$s igujj ɣer %2$s + Authentication does not work? + Here are some checks that might help:\n\n + - Check there is no spelling mistakes in the instance name\n\n + - Check that your instance is not down\n\n + - If you use the two-factor authentication (2FA), please use the link at the bottom (once the instance name is filled)\n\n + - You can also use this link without using the 2FA\n\n + - If it still does not work, please raise an issue on FramaGit at https://framagit.org/tom79/fedilab/issues + + Media has been loaded. Tap here to display it. + This action can be quite long. You will be notified when it will be finished. + Still running, please wait… + Export statuses + Export statuses for %1$s + %1$s toots out of %2$s have been exported. + Something went wrong when exporting data for %1$s + Something went wrong when exporting data! + Something went wrong when importing data! + + Apṛuksi + Rmed apṛuksi? + Asenneftaγ + Awwur + Isem n useqdac + Awal uffir + Add toot details when sharing + Support the app on Liberapay + There is an error in the regular expression! + No timelines was found on this instance! + Delete this instance? + Suqel s + Follow instance + You already follow this instance! + The instance is followed! + Partnerships + Talγut + Hide boosts from %s + Feature on profile + Show boosts from %s + Don\'t feature on profile + The account is now featured on profile + The account is no longer featured on profile + Boosts are now shown! + Boosts are now hidden! + Izen usrid + Imzizdigen + No filters to display. You can create one by tapping on the \"+\" button. + Keyword or phrase + Home timeline + Public timelines + Ilγuyen + Aqessaṛ + Will be matched regardless of casing in text or content warning of a toot + Drop instead of hide + Filtered toots will disappear irreversibly, even if filter is later removed + When the keyword or phrase is alphanumeric only, it will only be applied if it matches the whole word + Whole word + Filter contexts + One or multiple contexts where the filter should apply + Expire after + Kkes imzizdig? + Update filter + Snulfud imzizdig + Anwa ara ḍefṛeḍ + There is no accounts listed for the moment! + Ḍfeṛ + Fren kullec + Kkes kullec + %s is followed! + Creating the list %s + Adding accounts to the list + Accounts were added to the list + Adding accounts to the list + You have not created a list yet. Tap on the \"+\" button to add a new one. + Menhu ara ḍefṛeḍ + Trunk API + Account(s) can\'t be followed + Fetching remote account + Automatically expand hidden media + New follow + New Boost + New Favourite + New Mention + Poll Ended + New Toot + Toots Backup + New posts + Media Download + Change notification sound + Select Tone + Enable time slot + How To Videos + Yettalid uqesseṛ anemgag! + No blocked domains! + Serreḥ i taγult + Are you sure to unblock %s? + Are you sure to block %s?\n\nYou will not see any content from that domain in any public timeline or in your notifications. Your followers from that domain will be removed. + Blocked domains + Sewḥel taγult + The domain is blocked + The domain is no longer blocked! + Fetching remote status + Comment + Peertube instance + Be the first to leave a comment on this video with the top right button! + %s n tiẓriwin + Tangazt: %s + Add an instance + Comments are not enabled on this video! + Pick up a resolution + Imenyifen n PeerTube + The video has been added to bookmarks! + The video has been removed from bookmarks! + There is no Peertube videos in your favourites! + Abadu + Tividyutin + Ibuda + Use Emoji One + Talγut + Display previews in all toots + New UX/UI designer + Display video previews + The account id has been copied in the clipboard! + Beddel tutlayt + Tutlayt tamezwarut + Truncate long toots + Truncate toots over \'x\' lines. Zero means disabled. + Sken ddeqs + Sken kra kan + Sefrek tibzimin + The tag already exists! + The tag has been stored! + The tag has been changed! + The tag has been removed! + Schedule boost + The boost is scheduled! + No scheduled boost to display! + Schedule boost.]]> + Art timeline + Ldi umuγ + Uγal + Alugu n wesnas + Tugna n umaγnu + Profile banner + Contact admin of the instance + Rnu amaynut + Alugu n MastoHost + Amsefran n emuji + Smiren + Simγur aqesseṛ + Kkes amiḍan + Remove the blocked domain + Custom emoji picker + Seddu tavidyutt + Tijewwiqt tamaynutt + Image of the card + Hide media + Favicon + Add description for media (for the visually impaired) + + Werǧin + 30 n tesdatin + 1 n usrag + 6 n isragen + 12 n isragen + 1 wass + 1 umalas + + In this field, you need to write your instance host name.\nFor example, if you created your account on https://mastodon.social\nJust write mastodon.social (without https://)\n + You can start writing first letters and names will be suggested.\n\n + ⚠ The Login button will only work if the instance name is valid and the instance is up! + + Ugar n telγut + + Tutlayin + Media only + Show NSFW + Tisuqilin n Crowdin + Crowdin manager + Suqel asnas γer tutlayt nniḍen + Γef Crowdin + Buṭ + Pixelfed instance + Mastodon instance + Any of these + All of these + None of these + Any of these words (space-separated) + All these words (space-separated) + Add some words to filter (space-separated) + Beddel isem n wejgu + Misskey instance + No app supporting this link is installed on your device. + Subscriptions + Overview + Trending + Recently added + Local + Upload + Err + Kkes awennit + Tebγiḍ ad ekkeseḍ awennit-a? + Tabidyutt deg ugdil aččuṛan + Mode for videos + Select the file to upload + Tibidyutin-iw + Azwel + Turagt + Taggayt + Tutlayt + This video contains mature or explicit content + Enable video comments + Update video + Aglam + The video has been updated! + Upload cancelled! + The video has been uploaded! + Uploading, please wait… + Tap here to edit the video data. + Kkes tabidyutt + Are you sure to delete this video? + Display NSFW videos + Ulac kra n tibidyutin ara d-nesken! + Leave a comment + Bḍu + Choose a schedule mode + From device + From server + Toots (Server) + Toots (Device) + Modify + Display new toots above the \"Fetch more\" button + Timelines + Agrudem + Inermisen + %1$s commented your video %2$s]]> + %1$s is following your channel %2$s]]> + %1$s is following your account]]> + %1$s has been published]]> + %1$s succeeded]]> + %1$s failed]]> + %1$s published a new video: %2$s]]> + %1$s has been blacklisted]]> + %1$s has been unblacklisted]]> + Sifeḍ isefka + Ekter isefka + Select the file to import + An error occurred when selecting the backup file! + Add a public comment + Azen awennit + There is no Internet connection. Your message has been stored in drafts. + Aḍris aččuran + HTML + Markdown + Ffeɣ seg umiḍan + Akk + Support the app + Open Collective enables groups to quickly set up a collective, raise funds and manage them transparently. + Nγel aseγwen + Qqen + Amagnu + Compact + Console + Set display mode + Patch the Security Provider + Update tracking domains + The tracking data base has been updated! + http calls blocked by the application + List of blocked calls + Azen + The data base has been exported! + Featured hashtags + Filter timeline with tags + No tags + Hide the \"delete\" button in the notification tab + Attach an image when sharing a URL + + Poll + Polls + Create a poll + Afran 1 + Afran 2 + Afran %d + You need two choices at least for the poll! + Immed + end at %s + Refresh poll + Vote + A poll you have voted in has ended + A poll you tooted has ended + Customize + Taggayin + Time slot + Talqayt + Display \'new\' badge on unread toots + PeerTube + Move timeline + Hide timeline + Reorder timelines + List permanently deleted + Followed instance removed + Pinned tag removed + Sefsex + You need to keep two visible tabs! + Reorder timelines + Main timelines can only be hidden! + BBCode + Always mark media as sensitive + GNU instance + Cached status + Forward tags in replies + Long press to store media + Blur sensitive media + Display timelines in a list + Display timelines + Mark bot accounts in toots + Manage tags + Remember the position in Home timeline + Amazray + Playlists + Display name + You don\'t have any playlists. Tap on the \"+\" icon to add a new playlist + You must provide a display name! + The channel is required when the playlist is public. + Create a playlist + There is nothing in this playlist yet. + err-d + Gallery + Emoji + Sticker + Eraser + Aḍris + Filter + Brush + Are you sure you want to exit without saving the image? + Discard + Saving… + Image Saved Successfully! + Failed to save Image + Opacity + Enable photo editor + Add a poll item + Remove last poll item + Mute conversation + Unmute conversation + The conversation is no longer muted! + The conversation is muted + Open application features + Timed mute + Mention the account + Refresh cache + Mention the status + Isallen + General + Regional + Art + Journalism + Activism + Gaming + Tatiknulujit + Adult content + Furry + Tuččit + Logo of the instance + Something went wrong when checking available instances! + Zeddi deg Masṭudun + Choose an instance by picking up a category, then tap on a check button. + Choose an instance by tapping on a check button. + %1$s iseqdacen + Sentem awal uffir + I agree to %1$s and %2$s + server rules + terms of service + Sigez + This instance works with invitations. Your account will need to be manually approved by an administrator before being usable. + Please, fill all the fields! + Passwords don\'t match! + The email doesn\'t seem to be valid! + Your username will be unique on %1$s + You will be sent a confirmation e-mail + Use at least 8 characters + Password should contain at least 8 characters + Username should only contain letters, numbers and underscores + Account created! + Your account has been created!\n\n + Think to validate your email within the 48 next hours.\n\n + You can now connect your account by writing %1$s in the first field and tap on Connect.\n\n + Important: If your instance required validation, you will receive an email once it is validated! + + Save the message in drafts? + Administration + Ineqqisen + No reports to display! + Reconnect the account + The application failed to access the admin features. You might need to reconnect the account for having the correct scope. + Unresolved + Remote + Active + Pending + Disabled + Silenced + Suspended + Permissions + Email status + Login status + Joined + Most recent IP + Warn + Disable + Silence + Notify the user per e-mail + Custom warning + Aseqdac + Moderator + Administrator + Confirmed + Not confirmed + Reported statuses + Amiḍan + Undo silence + Undo disable + Suspend + Undo suspend + The account is silenced! + The account is no longer silenced! + The account is suspended! + The account is no longer suspended! + The account is disabled! + The account is no longer disabled! + The account has been warned! + Display the admin menu + Display the admin feature in statuses + Sireg + The account is approved! + The account is rejected! + Assign to me + Unassign + Mark as resolved + Mark as unresolved + Agbur d ilem! + Display Fedilab features button + The application needs to access audio recording + Voice message + Enable quick reply + The account you are replying might not see your message! + If disabled, the app will always load last statuses + If disabled, sensitive media will be hidden with a button + Store media in full size with a long press on previews + Add an ellipse button at the top right for listing all tags/instances/lists + During the time slot, the app will send notifications. You can reverse (ie: silent) this time slot with the right spinner. + Display a Fedilab button below profile picture. It is a shortcut for accessing in-app features. + Allow to reply directly in timelines below statuses + Previews will not be cropped in timelines + Allow to play embedded videos directly in timelines + Allow to reverse the way to read statuses that are displayed once tapping the fetch more button + This option allows to support recent cipher suites. It is useful for older Android devices or if you cannot connect to your instance. + Exclusively for Peertube videos. Switch this mode if you cannot play them. + These tags will allow to filter statuses from profiles. You will have to use the context menu for seeing them. + Automatically insert a line break after the mention to capitalize the first letter + Allow content creators to share statuses to their RSS feeds + Asuddes + Maximum retry times when uploading media + Create a new Folder here + Enter the folder name + Please enter a valid folder name + This folder already exists.\n Please provide another name for the folder + Select + Default Directory + Folder + Create folder + Display a toast message after an action has been completed (boost, fav, etc.)? + Muted instances have been exported! + Add an instance + Export instances + Import instances + Crash reports + Enable crash reports + If enabled, a crash report will be created locally and then you will be able to share it. + Yecceḍ Fedilab :( + You can send me by email the crash report. It will help to fix it :)\n\nYou can add additional content. Thank you! + Use the wysiwyg + When enabled, you will be able to format your text easily with tools. + Statistics + Total statuses + Number of boosts + Number of favourites + Number of mentions + Number of follows + Number of polls + Number of replies + Number of statuses + Statuses + Visibility + Number with media + Number with sensitive media + Number with CW + First status date + Last status date + First notification date + Last notification date + Frequency + %s statuses per day + %s notifications per day + Date range + Groups + No groups! + Disable custom animated emojis + Charts + Display charts + The application collects your local data, please wait… + Backup + Auto backup statuses + This option is per account. It will launch a service that will automatically store your statuses locally in the database. That allows to get statistics and charts + Auto backup notifications + This option is per account. It will launch a service that will automatically store your notifications locally in the database. That allows to get statistics and charts + Report account + Send an invitation + Your instance does not allow to register a new account! + + %d vote + %d votes + + + %d voter + %d voters + + + Single choice + Multiple choices + + + 5 n tesdatin + 30 n tesdatin + 1 n usrag + 6 n isragen + 1 wass + 3 n wussan + 7 n wussan + + + Webview + Direct stream + + For joining my instance \"%1$s\", you can download Fedilab:\n\nF-Droid: %2$s\nGoogle: %3$s\n\nThen open the link below with Fedilab and create your account :)\n\n%4$s + + Your poll can\'t have duplicated options! + For all accounts + Database cache + Clear your home timeline cache + Clear your cached statuses + Clear your bookmarks + Files in cache + Total notifications + Hide menu items + Fedilab is running live notifications + For %1$s accounts with %2$s events + Live notifications for %1$s + Live notifications will be enabled for this account. + Clear cache when leaving + The cache (media, cached messages, data from the built-in browser) will be automatically cleared when leaving the application. + Do you want to unfollow this account? + Show confirmation dialog before unfollowing + Replace Youtube with Invidio.us + Invidious is an alternative front-end to YouTube + Enter your custom host or leave blank for using invidious.snopyta.org + Replace Twitter with Nitter + Nitter is an open source alternative Twitter front-end focused on privacy. + Enter your custom host or leave blank for using nitter.net + Replace Instagram with Bibliogram + Bibliogram is an open source alternative Instagram front-end focused on privacy. + Enter your custom host or leave blank for using bibliogram.art + Replace Reddit with Libreddit + Libreddit is an open source alternative Reddit front-end focused on privacy. + Enter your custom host or leave blank for using libredd.it + Replace Medium links + Replace medium.com links with an open source alternative front-end focused on privacy. + Default: scribe.rip + Replace Wikipedia links + Replace Wikipedia link with an open source alternative front-end focused on privacy. + Default: wikiless.org + Hide Fedilab notification bar + For hiding the remaining notification in the status bar, tap on the eye icon button then uncheck: \"Display in status bar\" + Use a push notifications system for getting notifications in real time. + No live notifications + Live notifications + Notifications will be fetched every 15 minutes. + Add notes + Notes for the account + Allow to compress large photos into smaller sized photos with very less or negligible loss in quality of the image. + Allow compressing videos while maintaining their quality. + The app is compressing the media, it can be quite long… + Change app icon + Tap to change the app icon + Post + Visibility of the post + Tap here to add photos + Accepted Formats: jpeg, png, gif \n\nMax File Size: 15 MB \n\nAlbums can contain up to 4 photos or videos + Upload media + Add an optional caption + The app received a very long error message from the API %1$s + Message preview + Add mentions in each message + Fetching conversation + Order by + Title for the video + Join Peertube + I am at least 16 years old and agree to the %1$s of this instance + Links + Change the color of links (URLs, mentions, tags, etc.) in messages + Reblogs header + Change the color of display name at the top of messages + Change the color of the user name at the top of messages + Change the color of the header for reblogs + Posts + Background color of posts in timelines + Reset colors + Tap here to reset all your custom colors + Reset + Icons + Color of bottom icons in timelines + Pin this tag + Logo of the instance + Edit profile + Make an action + Translation + Image preview + Text color + Change the text color in pots + Apply changes + You need to restart the application to apply changes + Restart + Use a custom theme + Allow to override colors of the selected theme above + Theming + Store before + The theme was exported + The theme has been successfully exported in CSV + Apply the primary color to the status bar + Status bar color + Restore a default theme + Import a theme + Tap here to import a theme from a previous export + Export the theme + Tap here to export the current theme + An error occurred when selecting the theme file + Theme Picker + Select a pre-installed theme + Themes + Apply the primary color to the navigation bar + Navigation bar color + The underlying color of the app’s content. + Background color + Accents select parts of the UI. + Accent color + Displayed most frequently across your app. + Primary color + Export bookmarks to the instance + Import bookmarks from the instance + User count + Status count + Instance count + Blocked + End in %s + What\'s new in %s + You can follow my account for updates + This instance is not available on https://instances.social + Display full link + Share link + The URL has been copied to the clipboard + Open with another app + Check redirect + This URL does not redirect + %1$s \n\nredirects to\n\n %2$s + Change the user agent + Set a custom user agent or leave blank + Allows to customize the user agent used for api calls or with the built-in browser. + Remove UTM parameters + The app will automatically remove UTM parameters from URLs before visiting a link. + Trends + Trending now + %d people talking + Twitter accounts (via Nitter) + Twitter usernames space separated + Identity proofs + Verified identity + Verified by %1$s (%2$s) + Delete the notification + Display more options + It is a Pixelfed story + Upload a media, it will be automatically added to your Pixelfed story. + Media successfully added to your story! + Action disabled + Unfollow + Something went wrong, please check your download directory in settings. + Announcements + No announcements! + Add a reaction + Seqdec iminig-inek. m i tḥemmleḍ ar tama n usnas. Fren tamahlit-a akken ad ldin iseɣwan beṛṛa n usnas. + Video cache in MB, zero means no cache. + Watermarks + Automatically add a watermark at the bottom of pictures. The text can be customized for each account. + No distributors found! + You need a distributor for receiving push notifications.\nYou will find more details at %1$s.\n\nYou can also disable push notifications in settings for ignoring that message. + Select a distributor + diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml new file mode 100644 index 000000000..6ee8c7bdb --- /dev/null +++ b/app/src/main/res/values-ko/strings.xml @@ -0,0 +1,1134 @@ + + + 메뉴 열기 + 메뉴 닫기 + 정보 + 인스턴스 정보 + 개인정보 + 캐시 + 로그아웃 + 로그인 + + 닫기 + + 아니 + 취소 + 다운로드 + 다운로드 %1$s + 미디어 저장 완료 + 파일: %1$s + 비밀번호 + 이메일 + 계정 + + 태그 + 저장 + 복원 + 결과가 없습니다! + 인스턴스 + 인스턴스: mastodon.social + 계정 %1$s로 전환되었습니다 + 계정 추가 + 툿의 내용이 클립보드로 복사되었습니다 + 툿의 URL이 클립보드로 복사되었습니다 + 변경 + 사진을 선택... + 삭제 + 카메라 + 일괄 삭제 + 이 툿을 번역합니다. + 예약 + 텍스트 및 아이콘 크기 + 현재의 텍스트 크기를 변경: + 현재의 아이콘 크기를 변경: + 다음 + 이전 + 다음으로 열기 + 확인 + 미디어 + 공유 + Fedilab으로 공유 + 답글 + 사용자명 + 초안 + 즐겨찾기 + 새 팔로워 + 멘션 + 부스트 + 부스트 표시 + 답글 표시 + 브라우저에서 열기 + 번역 + 이 작업을 하기 전에 몇 초간 기다려주세요. + + + 로컬 타임라인 + 연합 타임라인 + 옵션 + 즐겨찾기 + 커뮤니케이션 + 뮤트한 사용자 + 차단한 사용자 + 알림 + 팔로우 요청 + 설정 + 계정 삭제 + 응용 프로그램에서 계정 %1$s를 삭제하시겠습니까? + 이메일 보내기 + 경로를 바꾸려면 누르세요 + 실패! + 예약된 툿 + 아래의 정보는 사용자의 정보를 완전하게 나타내지 못할 수도 있습니다. + 이모티콘 삽입 + The app did not collect custom emojis for the moment. + Push notifications + Are you sure you want to logout? + Are you sure you want to logout @%1$s@%2$s? + + 표시할 툿이 없습니다 + No stories to display + Stories + %1$s에 의해 부스트되었습니다 + 이 툿을 즐겨찾기에 추가하시겠습니까? + 이 툿을 즐겨찾기에서 삭제하시겠습니까? + 이 툿을 부스트하시겠습니까? + 이 툿의 부스트를 해제하시겠습니까? + 이 툿을 고정하시겠습니까? + 이 툿의 고정을 해제하시겠습니까? + 뮤트 + 차단 + 신고 + 삭제 + 복사 + 공유 + 멘션 + 시한 뮤트 + 삭제하고 다시 쓰기 + + 이 계정을 뮤트하시겠습니까? + 이 계정을 차단하시겠습니까? + 이 툿을 신고하시겠습니까? + 이 도메인을 차단하시겠습니까? + 이 계정의 뮤트를 해제하겠습니까? + Unblock this account? + + + 알림 + 무음 + + + 이 툿을 삭제하시겠습니까? + 이 툿을 삭제하고 다시 쓰시겠습니까? + + 북마크 + 북마크에 추가 + 북마크 삭제 + 북마크가 비어있습니다 + 게시물이 북마크에 추가 되었습니다! + 게시물이 북마크에서 삭제 되었습니다! + + %d 초 + %d 분 + %d 시 + %d 일 + + %d seconds + + + %d minutes + + + %d hours + + + %d days + + + 경고 + 무슨 생각을 하고 있나요? + 툿! + 큇! + 열람주의 + 툿 작성 + 답글 작성 + 큇 작성 + 답글 작성 + 미디어 선택 + 미디어를 선택하는 도중 오류가 발생하였습니다! + 이 미디어를 삭제할까요? + 툿 내용이 비어있습니다! + 툿 공개설정 + 툿의 기본 공개설정: + 툿이 성공적으로 보내졌습니다! + 이 툿에 대해 답글을 작성하고 있습니다: + 민감한 내용? + + 공개 타임라인에 작성 + 공개 타임라인에 비표시 + 팔로워에게만 공개 + 멘션 된 사용자들에게만 + + 임시 저장 된 것이 없습니다! + 툿을 선택하세요 + 계정을 선택하세요 + 계정들을 몇 개 선택하세요 + 임시 저장 된 글을 삭제할까요? + 버튼을 눌러 원본 툿 보기 + 시각장애인을 위한 설명 + + 설명이 존재하지 않습니다! + + 릴리즈 %1$s + 개발자: + 라이센스: + GNU GPL V3 + 소스 코드: + 툿 번역: + 인스턴스 검색: + 아이콘 디자이너 + + 대화 + + 표시할 계정이 없습니다 + 팔로우 요청이 없습니다 + 툿 \n %1$s + 팔로잉 \n %1$s + 팔로워 \n %1$s + 고정됨 \n %d + 권한 승인 + 거부 + + 예약 된 툿이 없습니다! + 툿을 작성한 뒤 상단 메뉴의 스케줄을 선택하세요. + 예약 된 툿을 지울까요? + 미디어: %d + 툿이 예약되었습니다! + 예약 날짜는 현재보다 미래여야 합니다! + 배터리 세이버가 활성화 되어 있습니다! 원하는대로 동작하지 않을 수 있습니다. + + 뮤트할 시간은 1분보다 길어야 합니다. + %1$s가 %2$s까지 뮤트 되었습니다.\n 상대의 프로필 페이지에서 뮤트를 취소할 수 있습니다. + %1$s는 %2$s까지 뮤트 되었습니다.\n 여기를 클릭하여 뮤트를 취소할 수 있습니다. + + 표시할 알림이 없습니다 + 당신을 언급했습니다 + wrote a new message + 당신의 게시물을 부스트 했습니다 + 당신의 상태를 마음에 들어했습니다 + 당신을 팔로우 했습니다 + asked to follow you + + 그리고 %d개의 다른 알림 + + + %d명이 좋아합니다 + + 알림을 삭제할까요? + 알림을 모두 삭제할까요? + 알림이 삭제되었습니다! + 알림이 모두 삭제되었습니다! + + 팔로잉 + 팔로워 + 고정됨 + + 클라이언트 ID를 얻을 수 없습니다! + 인스턴스 도메인에 연결 할 수 없습니다! + 인터넷에 연결되어 있지 않습니다! + 계정이 차단 되었습니다! + 계정이 차단 해제 되었습니다! + 계정이 뮤트 되었습니다! + 계정이 뮤트 해제 되었습니다! + 계정을 팔로우 하였습니다! + 계정의 팔로우를 해제했습니다! + 툿이 부스트 되었습니다! + The toot is no longer boosted! + The toot was added to your favourites! + The toot was removed from your favourites! + 툿이 신고 되었습니다! + 툿이 삭제 되었습니다! + 툿이 고정 되었습니다! + 툿이 고정 해제되었습니다! + 이런! 문제가 발생했습니다! + An error occurred! The instance did not return an authorisation code! + The instance domain does not seem to be valid! + An error occurred while switching between accounts! + An error occurred while searching! + The profile data have been saved! + 아무 조치를 취할 수 없습니다 + 미디어가 저장되었습니다! + An error occurred while translating! + Translations are disabled in settings + 초안이 저장 되었습니다! + Are you sure this instance allows this number of characters? Usually, this value is close to 500 characters. + Visibility of the toots has been changed for the account %1$s + + Number of toots per load + 항상 + Wi-Fi + 물어보기 + 미디어 불러오기 + 이미지 불러오기 + 더 보기… + 접기… + 민감한 내용 + GIF 아바타 비활성화 + 경로: + 자동으로 초안 저장 + 툿에 미디어 URL을 추가 + 누군가가 나를 팔로우 할 때 알림 + 누군가 내 게시물을 부스트 할 때 알림 + 누군가 내 게시물을 마음에 들어 할 때 알림 + 누군가 나를 언급할 때 알림 + 투표가 끝나면 알림 + Notify for new posts + 부스트 하기 전에 확인창 표시 + Show confirmation dialog before adding to favourites + Wi-Fi에 연결 되었을 때만 알림 + 일림? + Silent Notifications + NSFW 보는 시간(초, 0 = 비활성화) + Media Description timeout (seconds, 0 means off) + 프로필 수정 + Custom sharing + Your custom sharing URL… + 바이오… + 계정 잠그기 + 변경사항 저장 + 헤더 사진 선택 + 미리보기 이미지 크기 맞추기 + 이 글자 수를 넘기면 자동으로 답글을 통해 쪼갭니다: + 160자 글자 제한에 도달했습니다! + 30자 글자 제한에 도달했습니다! + 사이에 + 그리고 + 시간은 %1$s보다 길어야 합니다 + 시간은 %1$s보다 짧아야 합니다 + 시작 시간 + 끝날 시간 + 내장 브라우저 사용 + 커스텀 탭 + 자바스크립트 활성화 + CW를 자동으로 펼침 + 서드파티 쿠키 허용 + API 키, Yandex에 대해 공백으로 남겨 둘 수 있습니다 + + 어두움 + 밝음 + 블랙 + + LED 색 설정 + + 파란색 + 청록색 + 자홍색 + 초록색 + 빨간색 + 노란색 + 흰색 + + 팔로우 + 차단 해제 + 뮤트 + 뮤트 해제 + 요청 보내짐 + 당신을 팔로우 합니다 + 검색 + 답글의 첫 글자를 대문자로 + 이미지 크기 조정 + Resize videos + + 푸시 알림 + Please, confirm push notifications that you want to receive. + You can enable or disable these notifications later in settings (Notifications tab). + + + 캐시 지우기 + There are %1$s of data in cache.\n\nWould you like to delete them? + MB + Cache was cleared! %1$s were released + + 제목 + Title… + 설명 + 키워드 + 키워드… + + 동기화 + 필터 + 내 툿 + Your notifications + 공개 + 공개 타임라인에 비표시 + 비공개 + 다이렉트 + 키워드… + 미디어 보기 + 고정된 항목 보기 + No matching result found! + Backup toots for %1$s + %1$s new toots have been imported + %1$s new notifications have been imported + + Dates descending + Dates ascending + + + 아니오 + 오직 + 모두 + + No toots were found in database. Please, use the synchronize button from the menu to retrieve them. + + Recorded data + Only basic information from accounts are stored on the device. + These data are strictly confidential and can only be used by the application. + Deleting the application immediately removes these data.\n + ⚠ Login and passwords are never stored. They are only used during a secure authentication (SSL) with an instance. + + 권한: + - ACCESS_NETWORK_STATE: Used to detect if the device is connected to a WIFI network.\n + - INTERNET: Used for queries to an instance.\n + - WRITE_EXTERNAL_STORAGE: Used to store media or to move the app on a SD card.\n + - READ_EXTERNAL_STORAGE: Used to add media to toots.\n + - BOOT_COMPLETED: Used to start the notification service.\n + - WAKE_LOCK: Used during the notification service. + + API 권한: + - Read: Read data.\n + - Write: Post statuses and upload media for statuses.\n + - Follow: Follow, unfollow, block, unblock.\n\n + ⚠ These actions are carried out only when user requests them. + + 추적 및 라이브러리 + The application does not use tracking tools (audience measurement, error reporting, etc.) and does not contain any advertising.\n\n + The use of libraries is minimized: \n + - Glide: To manage media\n + - Android-Job: To manage services\n + - PhotoView: To manage images\n + + 툿 번역 + The application offers the ability to translate toots using the locale of the device and the Yandex API.\n + Yandex has its proper privacy-policy which can be found here: https://yandex.ru/legal/confidential/?lang=en + + Thank you to: + + 정규식에 의한 걸러내기 + 검색 + 삭제 + 툿 더 가져오기… + + 목록 + 이 리스트를 정말로 영원히 삭제하시겠습니까? + There is nothing in this list yet. When members of this list post new statuses, they will appear here. + 리스트에 추가 + 리스트 추가 + 리스트 삭제 + 리스트 수정 + 새 리스트 제목 + The account was added to the list! + You don\'t have any lists yet! + + %1$s은 %2$s로 이동했습니다 + Authentication does not work? + Here are some checks that might help:\n\n + - Check there is no spelling mistakes in the instance name\n\n + - Check that your instance is not down\n\n + - If you use the two-factor authentication (2FA), please use the link at the bottom (once the instance name is filled)\n\n + - You can also use this link without using the 2FA\n\n + - If it still does not work, please raise an issue on FramaGit at https://framagit.org/tom79/fedilab/issues + + Media has been loaded. Tap here to display it. + This action can be quite long. You will be notified when it will be finished. + Still running, please wait… + 게시물 내보내기 + Export statuses for %1$s + %1$s toots out of %2$s have been exported. + Something went wrong when exporting data for %1$s + Something went wrong when exporting data! + Something went wrong when importing data! + + 프록시 + 프록시 활성화? + 호스트 + 포트 + 로그인 + 비밀번호 + 공유할 때 툿의 세부 정보 포함 + Support the app on Liberapay + There is an error in the regular expression! + No timelines was found on this instance! + Delete this instance? + Translate in + Follow instance + You already follow this instance! + The instance is followed! + Partnerships + Information + Hide boosts from %s + Feature on profile + Show boosts from %s + Don\'t feature on profile + The account is now featured on profile + The account is no longer featured on profile + Boosts are now shown! + Boosts are now hidden! + Direct message + Filters + No filters to display. You can create one by tapping on the \"+\" button. + Keyword or phrase + Home timeline + Public timelines + Notifications + Conversations + Will be matched regardless of casing in text or content warning of a toot + 숨기는 대신 삭제 + 필터링 된 툿은 필터가 삭제 된 뒤에도 돌아오지 않습니다. + When the keyword or phrase is alphanumeric only, it will only be applied if it matches the whole word + Whole word + Filter contexts + One or multiple contexts where the filter should apply + 만료 시간 + 필터를 삭제할까요? + 필터 갱신 + 필터 만들기 + 팔로우 할 사람들 + There is no accounts listed for the moment! + 팔로우 + 모두 선택 + 모두 선택 취소 + %s is followed! + Creating the list %s + Adding accounts to the list + Accounts were added to the list + Adding accounts to the list + You have not created a list yet. Tap on the \"+\" button to add a new one. + Who to follow + Trunk API + Account(s) can\'t be followed + Fetching remote account + Automatically expand hidden media + 새 팔로우 + 새 부스트 + 새 즐겨찾기 + 새로운 언급 + 투표 종료 + 새 툿 + 툿 백업 + New posts + 미디어 다운로드 + 알림음 변경 + Select Tone + Enable time slot + How To Videos + 원격 글타래 불러오는 중! + 차단 된 도메인이 없습니다! + 도메인 차단 해제 + 정말로 %s의 차단을 해제할까요? + Are you sure to block %s?\n\nYou will not see any content from that domain in any public timeline or in your notifications. Your followers from that domain will be removed. + 차단 된 도메인 + 도메인 차단 + 도메인이 차단 되었습니다 + The domain is no longer blocked! + Fetching remote status + Comment + Peertube instance + Be the first to leave a comment on this video with the top right button! + %s views + Duration: %s + Add an instance + Comments are not enabled on this video! + Pick up a resolution + Peertube favourites + The video has been added to bookmarks! + The video has been removed from bookmarks! + There is no Peertube videos in your favourites! + 채널 + 동영상 + 채널 + Use Emoji One + 정보 + Display previews in all toots + New UX/UI designer + Display video previews + 계정의 아이디가 클립보드에 복사되었습니다! + 언어 변경 + 기본 언어 + 긴 툿 자르기 + \'x\'줄을 넘는 툿을 자릅니다. 0으로 설정하면 비활성화 됩니다. + 더 보기 + 덜 보기 + 태그 관리 + 태그가 이미 존재합니다! + 태그가 저장되었습니다! + 태그가 변경되었습니다! + 태그가 삭제되었습니다! + 예약된 부스트 + 부스트가 예약되었습니다! + 예약 된 부스트가 없습니다! + Schedule boost.]]> + 예술 타임라인 + 메뉴 열기 + 뒤로 가기 + 응용프로그램 로고 + 프로필 사진 + 프로필 배너 + 인스턴스의 관리자에게 연락 + 새로 추가 + MastoHost 로고 + 에모지 선택기 + 새로고침 + 대화 확장하기 + 계정 삭제 + 도메인 차단 삭제 + 커스텀 에모지 선택기 + 동영상 재생 + 새 툿 + 카드 이미지 + 미디어 숨기기 + 파비콘 + 미디어 설명 추가 (시각장에인을 위해) + + 영원히 + 30 분 + 1 시간 + 6 시간 + 12 시간 + 1 일 + 1 주 + + In this field, you need to write your instance host name.\nFor example, if you created your account on https://mastodon.social\nJust write mastodon.social (without https://)\n + You can start writing first letters and names will be suggested.\n\n + ⚠ The Login button will only work if the instance name is valid and the instance is up! + + 더 많은 정보 + + 언어 + 미디어만 + NSFW 보기 + Crowdin 번역 + Crowdin 매니저 + 응용프로그램 번역 + Crowdin에 대하여 + + 픽셀페드 인스턴스 + 마스토돈 인스턴스 + Any of these + All of these + None of these + Any of these words (space-separated) + All these words (space-separated) + Add some words to filter (space-separated) + Change column name + Misskey instance + No app supporting this link is installed on your device. + Subscriptions + Overview + Trending + Recently added + Local + Upload + Reply + Delete a comment + Are you sure to delete this comment? + Full screen video + Mode for videos + Select the file to upload + My videos + Title + License + Category + 언어 + This video contains mature or explicit content + Enable video comments + Update video + Description + The video has been updated! + Upload cancelled! + The video has been uploaded! + Uploading, please wait… + Tap here to edit the video data. + Delete video + Are you sure to delete this video? + Display NSFW videos + No videos to display! + Leave a comment + Share + 예약 모드 선택 + 장치에서 + 서버에서 + 툿(서버) + 툿(장치) + 변경 + \"새로 가져오기\" 버튼 위에 새 툿 표시 + 타임라인 + 인터페이스 + 연락처 + %1$s commented your video %2$s]]> + %1$s is following your channel %2$s]]> + %1$s is following your account]]> + %1$s has been published]]> + %1$s succeeded]]> + %1$s failed]]> + %1$s published a new video: %2$s]]> + %1$s has been blacklisted]]> + %1$s has been unblacklisted]]> + 데이터 내보내기 + 데이터 가져오기 + 가져 올 파일을 선택하세요 + An error occurred when selecting the backup file! + 공개 댓글 추가 + 댓글 보내기 + There is no Internet connection. Your message has been stored in drafts. + 평문 + HTML + 마크다운 + 계정 로그아웃 + 모두 + 응용프로그램 지원하기 + Open Collective enables groups to quickly set up a collective, raise funds and manage them transparently. + 링크 복사 + 연결 + 보통 + 간결 + 콘솔 + 표시 모드 설정 + 보안 제공자(Security Provider) 패치 + 추적 도메인 갱신 + 추적 데이터가 갱신되었습니다! + http 호출이 응용프로그램에 의해 차단 되었습니다 + 차단 된 호출 내역 + 제출 + 데이터가 내보내졌습니다 + 추천 해시태그 + 타임라인을 태그로 필터링 + No tags + Hide the \"delete\" button in the notification tab + Attach an image when sharing a URL + + Poll + Polls + Create a poll + 선택 1 + Choice 2 + Choice %d + You need two choices at least for the poll! + Done + end at %s + Refresh poll + Vote + A poll you have voted in has ended + A poll you tooted has ended + Customize + 카테고리 + Time slot + 고급 + Display \'new\' badge on unread toots + Peertube + Move timeline + Hide timeline + Reorder timelines + List permanently deleted + Followed instance removed + Pinned tag removed + Undo + You need to keep two visible tabs! + Reorder timelines + Main timelines can only be hidden! + BBCode + Always mark media as sensitive + 그누 인스턴스 + 캐시 된 게시물 + 답글에 태그 전달 + 길게 눌러 미디어 저장 + 민감한 미디어를 흐리게 표시 + 타임라인을 리스트로 표시 + 타임라인 표시 + 툿에 봇 계정 표시 + 태그 관리 + 홈 타임라인의 위치 기억하기 + 내역 + 재생목록 + 표시되는 이름 + You don\'t have any playlists. Tap on the \"+\" icon to add a new playlist + You must provide a display name! + The channel is required when the playlist is public. + Create a playlist + There is nothing in this playlist yet. + 다시 실행 + 갤러리 + 에모지 + 스티커 + 지우개 + 문자 + 필터 + + 저장하지 않고 종료할까요? + 버리기 + 저장… + 성공적으로 저장되었습니다! + 저장하는데 실패하였습니다 + 불투명도 + 사진 편집기 활성화 + 투표 항목 추가 + 마지막 투표 항목 지우기 + 대화 뮤트 + 대화 뮤트 해제 + The conversation is no longer muted! + The conversation is muted + Open application features + Timed mute + Mention the account + 캐시 새로고침 + 게시물 언급하기 + News + General + Regional + Art + Journalism + Activism + Gaming + Technology + Adult content + Furry + Food + Logo of the instance + Something went wrong when checking available instances! + Join Mastodon + Choose an instance by picking up a category, then tap on a check button. + Choose an instance by tapping on a check button. + %1$s users + Confirm password + I agree to %1$s and %2$s + server rules + terms of service + Sign up + This instance works with invitations. Your account will need to be manually approved by an administrator before being usable. + Please, fill all the fields! + Passwords don\'t match! + The email doesn\'t seem to be valid! + Your username will be unique on %1$s + You will be sent a confirmation e-mail + Use at least 8 characters + Password should contain at least 8 characters + Username should only contain letters, numbers and underscores + Account created! + Your account has been created!\n\n + Think to validate your email within the 48 next hours.\n\n + You can now connect your account by writing %1$s in the first field and click on Connect.\n\n + Important: If your instance required validation, you will receive an email once it is validated! + + Save the message in drafts? + Administration + Reports + No reports to display! + Reconnect the account + The application failed to access the admin features. You might need to reconnect the account for having the correct scope. + Unresolved + Remote + Active + Pending + Disabled + Silenced + Suspended + Permissions + Email status + Login status + Joined + Most recent IP + Warn + Disable + Silence + Notify the user per e-mail + Custom warning + User + Moderator + Administrator + Confirmed + Not confirmed + Reported statuses + Account + Undo silence + Undo disable + Suspend + Undo suspend + The account is silenced! + The account is no longer silenced! + The account is suspended! + The account is no longer suspended! + The account is disabled! + The account is no longer disabled! + The account has been warned! + Display the admin menu + Display the admin feature in statuses + Allow + The account is approved! + The account is rejected! + Assign to me + Unassign + Mark as resolved + Mark as unresolved + Empty content! + Display Fedilab features button + The application needs to access audio recording + Voice message + Enable quick reply + The account you are replying might not see your message! + If disabled, the app will always load last statuses + If disabled, sensitive media will be hidden with a button + Store media in full size with a long press on previews + Add an ellipse button at the top right for listing all tags/instances/lists + During the time slot, the app will send notifications. You can reverse (ie: silent) this time slot with the right spinner. + Display a Fedilab button below profile picture. It is a shortcut for accessing in-app features. + Allow to reply directly in timelines below statuses + Previews will not be cropped in timelines + Allow to play embedded videos directly in timelines + Allow to reverse the way to read statuses that are displayed once tapping the fetch more button + This option allows to support recent cipher suites. It is useful for older Android devices or if you cannot connect to your instance. + Exclusively for Peertube videos. Switch this mode if you cannot play them. + These tags will allow to filter statuses from profiles. You will have to use the context menu for seeing them. + Automatically insert a line break after the mention to capitalize the first letter + Allow content creators to share statuses to their RSS feeds + Compose + Maximum retry times when uploading media + Create a new Folder here + Enter the folder name + Please enter a valid folder name + This folder already exists.\n Please provide another name for the folder + Select + Default Directory + Folder + Create folder + Display a toast message after an action has been completed (boost, fav, etc.)? + Muted instances have been exported! + Add an instance + Export instances + Import instances + Crash reports + Enable crash reports + If enabled, a crash report will be created locally and then you will be able to share it. + Fedilab has stopped :( + You can send me by email the crash report. It will help to fix it :)\n\nYou can add additional content. Thank you! + Use the wysiwyg + When enabled, you will be able to format your text easily with tools. + Statistics + Total statuses + Number of boosts + Number of favourites + Number of mentions + Number of follows + Number of polls + Number of replies + Number of statuses + Statuses + Visibility + Number with media + Number with sensitive media + Number with CW + First status date + Last status date + First notification date + Last notification date + Frequency + %s statuses per day + %s notifications per day + Date range + Groups + No groups! + Disable custom animated emojis + Charts + Display charts + The application collects your local data, please wait… + Backup + Auto backup statuses + This option is per account. It will launch a service that will automatically store your statuses locally in the database. That allows to get statistics and charts + Auto backup notifications + This option is per account. It will launch a service that will automatically store your notifications locally in the database. That allows to get statistics and charts + Report account + Send an invitation + Your instance does not allow to register a new account! + + %d votes + + + %d voters + + + Single choice + Multiple choices + + + 5 분 + 30 분 + 1 시간 + 6 시간 + 1 일 + 3 일 + 7 일 + + + Webview + Direct stream + + For joining my instance \"%1$s\", you can download Fedilab:\n\nF-Droid: %2$s\nGoogle: %3$s\n\nThen open the link below with Fedilab and create your account :)\n\n%4$s + + Your poll can\'t have duplicated options! + For all accounts + Database cache + Clear your home timeline cache + Clear your cached statuses + Clear your bookmarks + Files in cache + Total notifications + Hide menu items + Fedilab is running live notifications + For %1$s accounts with %2$s events + Live notifications for %1$s + Live notifications will be enabled for this account. + Clear cache when leaving + The cache (media, cached messages, data from the built-in browser) will be automatically cleared when leaving the application. + Do you want to unfollow this account? + Show confirmation dialog before unfollowing + Replace Youtube with Invidio.us + Invidious is an alternative front-end to YouTube + Enter your custom host or leave blank for using invidious.snopyta.org + Replace Twitter with Nitter + Nitter is an open source alternative Twitter front-end focused on privacy. + Enter your custom host or leave blank for using nitter.net + Replace Instagram with Bibliogram + Bibliogram is an open source alternative Instagram front-end focused on privacy. + Enter your custom host or leave blank for using bibliogram.art + Replace Reddit with Libreddit + Libreddit is an open source alternative Reddit front-end focused on privacy. + Enter your custom host or leave blank for using libredd.it + Replace Medium links + Replace medium.com links with an open source alternative front-end focused on privacy. + Default: scribe.rip + Replace Wikipedia links + Replace Wikipedia link with an open source alternative front-end focused on privacy. + Default: wikiless.org + Hide Fedilab notification bar + For hiding the remaining notification in the status bar, tap on the eye icon button then uncheck: \"Display in status bar\" + Use a push notifications system for getting notifications in real time. + No live notifications + Live notifications + Notifications will be fetched every 15 minutes. + Add notes + Notes for the account + Allow to compress large photos into smaller sized photos with very less or negligible loss in quality of the image. + Allow compressing videos while maintaining their quality. + The app is compressing the media, it can be quite long… + Change app icon + Tap to change the app icon + Post + Visibility of the post + Tap here to add photos + Accepted Formats: jpeg, png, gif \n\nMax File Size: 15 MB \n\nAlbums can contain up to 4 photos or videos + Upload media + Add an optional caption + The app received a very long error message from the API %1$s + Message preview + Add mentions in each message + Fetching conversation + Order by + Title for the video + Join Peertube + I am at least 16 years old and agree to the %1$s of this instance + Links + Change the color of links (URLs, mentions, tags, etc.) in messages + Reblogs header + Change the color of display name at the top of messages + Change the color of the user name at the top of messages + Change the color of the header for reblogs + Posts + Background color of posts in timelines + Reset colors + Tap here to reset all your custom colors + Reset + Icons + Color of bottom icons in timelines + Pin this tag + Logo of the instance + Edit profile + Make an action + Translation + Image preview + Text color + Change the text color in pots + Apply changes + You need to restart the application to apply changes + Restart + Use a custom theme + Allow to override colors of the selected theme above + Theming + Store before + The theme was exported + The theme has been successfully exported in CSV + Apply the primary color to the status bar + Status bar color + Restore a default theme + Import a theme + Tap here to import a theme from a previous export + Export the theme + Tap here to export the current theme + An error occurred when selecting the theme file + Theme Picker + Select a pre-installed theme + Themes + Apply the primary color to the navigation bar + Navigation bar color + The underlying color of the app’s content. + Background color + Accents select parts of the UI. + Accent color + Displayed most frequently across your app. + Primary color + Export bookmarks to the instance + Import bookmarks from the instance + User count + Status count + Instance count + Blocked + End in %s + What\'s new in %s + You can follow my account for updates + This instance is not available on https://instances.social + Display full link + Share link + The URL has been copied to the clipboard + Open with another app + Check redirect + This URL does not redirect + %1$s \n\nredirects to\n\n %2$s + Change the user agent + Set a custom user agent or leave blank + Allows to customize the user agent used for api calls or with the built-in browser. + Remove UTM parameters + The app will automatically remove UTM parameters from URLs before visiting a link. + Trends + Trending now + %d people talking + Twitter accounts (via Nitter) + Twitter usernames space separated + Identity proofs + Verified identity + Verified by %1$s (%2$s) + Delete the notification + Display more options + It is a Pixelfed story + Upload a media, it will be automatically added to your Pixelfed story. + Media successfully added to your story! + Action disabled + Unfollow + Something went wrong, please check your download directory in settings. + Announcements + No announcements! + Add a reaction + Use your favourite browser inside the app. Uncheck this feature to open links externally. + Video cache in MB, zero means no cache. + Watermarks + Automatically add a watermark at the bottom of pictures. The text can be customized for each account. + No distributors found! + You need a distributor for receiving push notifications.\nYou will find more details at %1$s.\n\nYou can also disable push notifications in settings for ignoring that message. + Select a distributor + diff --git a/app/src/main/res/values-land/dimens.xml b/app/src/main/res/values-land/dimens.xml new file mode 100644 index 000000000..6d802f25a --- /dev/null +++ b/app/src/main/res/values-land/dimens.xml @@ -0,0 +1,8 @@ + + 48dp + 16dp + 16dp + 8dp + 176dp + 20dp + \ No newline at end of file diff --git a/app/src/main/res/values-ml/strings.xml b/app/src/main/res/values-ml/strings.xml new file mode 100644 index 000000000..d3e3be951 --- /dev/null +++ b/app/src/main/res/values-ml/strings.xml @@ -0,0 +1,1140 @@ + + + മെനു തുറക്കുക + മെനു അടയ്ക്കുക + വിവരം + ഇതിനെക്കുറിച്ചുള്ള വിവരങ്ങൾ + സ്വകാര്യത + കാഷേ + പുറത്തിറങ്ങുക + അകത്തു കയറുക + + അടയ്ക്കുക + ശരി + വേണ്ട + റദ്ദാക്കുക + ഇറക്കിവെക്കുക + ഡൗൺലോഡ് %1$s + മാധ്യമം സൂക്ഷിക്കപ്പെട്ടിരിക്കുന്നു + ഫയൽ %1$s + രഹസ്യവാക്ക് + ഇമെയിൽ + അക്കൗണ്ടുകൾ + ടൂട്ടുകൾ + ടാഗുകൾ + സൂക്ഷിക്കൂ + പുനഃസ്ഥാപിക്കുക + ഫലങ്ങൾ ഇല്ല! + പതിപ്പ് + പതിപ്പ്: mastodon.social + ഇപ്പോൾ അക്കൗണ്ടിന്റെ കൂടെ പ്രവർത്തിക്കുന്നു %1$s + ഒരു അക്കൗണ്ട് ചേർക്കുക + ഈ ടൂട്ടിന്റെ ഉള്ളടക്കം ക്ലിപ്ബോർഡിലേക്ക് പകർത്തിയിരിക്കുന്നു + ഈ ടൂട്ടിന്റെ യു ആർ എൽ ക്ലിപ്ബോർഡിലേക്ക് പകർത്തിയിരിക്കുന്നു + മാറ്റുക + ഒരു പടം തിരഞ്ഞെടുക്കൂ… + വൃത്തിയാക്കൂ + ക്യാമറ + എല്ലാം മായ്ക്കുക + ഈ ടൂട്ടിനെ മൊഴി മാറ്റം നടത്തുക. + കാര്യക്രമം + അക്ഷരങ്ങളുടെയും ചിഹ്നങ്ങളുടെയും അളവുകൾ + അക്ഷരത്തിന്റെ അളവ് മാറ്റുക: + ചിഹ്നത്തിന്റെ അളവ് മാറ്റുക: + അടുത്തത് + മുൻപത്തേത് + ഇതുപയോഗിച്ച് തുറക്കുക + നിർണ്ണയം + വേദിക + ഇവരുമായി പങ്കിടുക + ഫെഡിലാബ് വഴി പങ്കിടുക + മറുപടികൾ + ഉപയോക്തൃ നാമം + ഡ്രാഫ്റ്റുകൾ + പ്രിയപ്പെട്ടവ + പുതിയ അനുയായികൾ + പ്രതിപാദനങ്ങൾ + ബൂസ്റ്റ് + ബൂസ്റ്റ് കാണിക്കുക + മറുപടി കാണിക്കുക + ബ്രൗസറിൽ തുറക്കുക + വിവർത്തനം + ദയവായി, ഈ നടപടി എടുക്കുന്നതിനു മുമ്പ് കുറച്ച് സെക്കൻഡ് കാത്തിരിക്കുക. + + വീട് + പ്രാദേശിക സമയരേഖ + ഫെഡറേറ്റഡ് സമയരേഖ + ഓപ്ഷനുകൾ + പ്രിയപ്പെട്ടവ + ആശയവിനിമയം + നിശബ്ദമാക്കപ്പെട്ട ഉപയോക്താക്കൾ + തടയപ്പെട്ട ഉപയോക്താക്കൾ + അറിയിപ്പുകൾ + പിന്തുടരാൻ ഉള്ള അഭ്യർത്ഥനകൾ + ക്രമീകരണങ്ങള്‍ + അക്കൗണ്ട് ഇല്ലാതാക്കുക + ഈ അപ്പ്ളിക്കേഷനിൽ നിന്നും %1$s അക്കൗണ്ട് നീക്കം ചെയ്യട്ടെ? + ഇ മെയിൽ അയക്കൂ + മാറ്റണമെങ്കിൽ ഇവിടെ ക്ലിക്ക് ചെയ്യൂ + പരാജയപ്പെട്ടു! + മുൻകൂട്ടി നിശ്ചയിച്ച സമയപ്രകാരം ഉള്ള ടൂട്ടുകൾ + താഴെ കൊടുത്തിരിക്കുന്ന വിവരങ്ങൾ ഈ ഉപയോക്താവിന്റെ പ്രൊഫൈൽ അപൂർണ്ണമായി പ്രതിഫലിപ്പിക്കുന്നു. + ഇമോജി ഇടൂ + ഈ ആപ് ഇപ്പോൾ വ്യക്തി അധിഷ്ഠിത ഇമോജികൾ സ്വീകരിക്കുന്നതല്ല. + Push notifications + നിങ്ങൾക്ക് ലോഗൗട്ട് ചെയ്യണമെന്ന് ഉറപ്പാണോ? + \@%1$s@%2$s ൽ നിന്നും നിങ്ങൾ ലോഗൗട്ട് ചെയ്യണമെന്ന് തീർച്ചയാണോ? + + പ്രദർശിപ്പിക്കാൻ ടൂട്ടുകൾ ഇല്ല + പ്രദർശിപ്പിക്കാൻ കഥകളൊന്നുമില്ല + കഥകൾ + ബൂസ്റ്റ് ചെയ്തത് %1$s + ഈ ടൂട്ടിനെ പ്രിയപ്പെ ട്ടവയിലേക്ക് ചേർക്കട്ടെ? + ഈ ടൂട്ടിനെ പ്രിയപ്പെ ട്ടവയിൽ നിന്ന് മാറ്റട്ടെ? + ഈ ടൂട്ടിനെ ബൂസ്റ്റ് ചെയ്യട്ടെ? + ഈ ടൂട്ടിൻറെ ബൂസ്റ്റ് നീക്കം ചെയ്യട്ടെ? + ഈ ടൂട്ട് പിൻചെയ്തു വയ്ക്കട്ടെ? + ഈ ടൂട്ട് പിൻചെയ്തത് നീക്കം ചെയ്യട്ടെ? + നിശബ്ദം + തടയുക + റിപ്പോര്‍ട്ടുചെയ്യുക + നീക്കുക + പകര്‍ത്തുക + പങ്കിടുക + പ്രതിപാദിക്കുക + സമയബന്ധിതമായി നിശബ്ദമാക്കുക + കളയുക & വീണ്ടും എഴുതുക + + ഈ അക്കൗണ്ട് നിശബ്ദമാക്കട്ടെ? + ഈ അക്കൗണ്ട് തടയട്ടെ? + ഈ ടൂട്ടിനെ റിപ്പോർട്ട് ചെയ്യട്ടെ? + ഈ ഡൊമെയിൻ തടയട്ടെ? + ഈ അക്കൗണ്ട് ശബ്ദിക്കട്ടെ? + Unblock this account? + + + അറിയിക്കുക + നിശബ്‌ദം + + + ഈ ടൂട്ടിനെ നീക്കം ചെയ്യട്ടെ? + ഈ ടൂത്ത് മായ്ച്ചു കളഞ്ഞു ആദ്യമേ എഴുതട്ടെ? + + ബുക്ക്മാർക്കുകൾ + ബുക്ക്‌മാർക്കിലേക്ക് ചേർക്കൂ + ബുക്ക്മാർക്ക് നീക്കംചെയ്യുക + പ്രദർശിപ്പിക്കാൻ ബുക്ക്മാർക്കുകൾ ഇല്ല + സ്റ്റാറ്റസ് ബുക്ക്മാർക്ക് ചെയ്യപ്പെട്ടിരിക്കുന്നു! + സ്റ്റാറ്റസ് ബുക്മാർക്കുകളിൽ നിന്ന് നീക്കം ചെയ്യപ്പെട്ടിരിക്കുന്നു! + + %d സെ + %d മി + %d ഹ + %d ഡേ + + %d സെക്കന്റ് + %d സെക്കന്റുകൾ + + + %d മിനിറ്റ് + %d മിനിറ്റുകൾ + + + %d മണിക്കൂർ + %d മണിക്കുറുകള്‍ + + + %d ദിവസം + %d ദിവസങ്ങള്‍ + + + മുന്നറിയിപ്പ് + നിങ്ങളുടെ മനസ്സിൽ എന്താണ്? + ടൂട്ട്! + ക്വീറ്റ്! + മുന്നറിയിപ്പ് എഴുതൂ + ടൂട്ട് എഴുതൂ + ടൂട്ടിന് മറുപടി എഴുതൂ + ക്വീറ്റ് എഴുതൂ + ക്വീറ്റിന് മറുപടി എഴുതൂ + ഒരു മാധ്യമം തിരഞ്ഞെടുക്കൂ + മാധ്യമം തിരഞ്ഞെടുത്തതിൽ പിഴവ് പറ്റിയിരിക്കുന്നു! + ഈ മാധ്യമം കളയട്ടെ? + താങ്കളുടെ ടൂട്ട് ശൂന്യമാണ്! + ടൂട്ടിന്റെ ദർശനീയത + ടൂട്ടിന്റെ മൗലികമായ ദർശനീയത: + ടൂട്ട് അയക്കപ്പെട്ടിരിക്കുന്നു! + തങ്ങൾ ഈ ടൂട്ടിനു മറുപടി എഴുതുകയാണ്: + അസ്വസ്ഥമാക്കാമാകുന്ന ഉള്ളടക്കം? + + പൊതു സമയ രേഖകളിലേക്ക് അയക്കുക + പൊതു സമയ രേഖകളിലേക്ക് അയക്കരുത് + പിന്തുടരുന്നവർക്ക് മാത്രം അയക്കുക + പരാമർശിക്കപ്പെട്ട ഉപയോക്താക്കൾക്ക് മാത്രം അയക്കുക + + ഡ്രാഫ്റ്റുകൾ ഇല്ല! + ഒരു ടൂട്ട് തിരഞ്ഞെടുക്കുക + ഒരു അക്കൗണ്ട് തിരഞ്ഞെടുക്കുക + ചില അക്കൗണ്ടുകൾ തിരഞ്ഞെടുക്കൂ + ഡ്രാഫ്റ്റ് നീക്കം ചെയ്യട്ടെ? + ഒറിജിനൽ ടൂട്ട് കാണാൻ ഈ ബട്ടണിൽ ഞെക്കൂ + കാഴ്ചശക്തി ഇല്ലാത്തവർക്ക് വേണ്ടി വിവരണം നൽകൂ + + വിവരണം ലഭ്യമല്ല! + + റിലീസ് %1$s + ഡെവലപ്പർ: + ലൈസന്‍സ്: + ഗ്നു ജി പി എൽ V3 + മൂലാധാരം: + ടൂട്ടുകളുടെ മൊഴി മാറ്റം: + പതിപ്പുകൾ തിരഞ്ഞു നോക്കൂ: + ചിഹ്നം രൂപകൽപന ചെയ്തത്: + + സംഭാഷണങ്ങൾ + + പ്രദർശിപ്പിക്കാൻ അക്കൗണ്ട് ഇല്ല + പിന്തുടരാനുള്ള അഭ്യർത്ഥനകൾ ഇല്ല + ടൂട്ട്സ് \n %1$s + പിന്തുടരുന്നു \n %1$s + പിന്തുടർച്ചക്കാർ \n %1$s + പിൻ കുത്തി വച്ചിരിക്കുന്നു \n %d + അംഗീകരണം + നിരാകരിക്കുക + + കാണിക്കാൻ സമയബന്ധിത ടൂട്ടുകൾ ഇല്ല! + ടൂട്ട് എഴുതിയ മുകളിൽ മെനുവിൽ നിന്ന് ഷെഡ്യൂൾ തിരഞ്ഞെടുക്കുക. + സമയബന്ധിത ടൂട്ട് നീക്കം ചെയ്യട്ടെ? + മാധ്യമം: %d + ടൂട്ട് ഷെഡ്യൂൾ ചെയ്യപ്പെട്ടു! + ഷെഡ്യൂൾ ചെയ്ത സമയം ഇപ്പോഴുള്ള സമയത്തേക്കാൾ വൈകി ആയിരിക്കണം! + ബാറ്ററി സേവർ പ്രവർത്തിക്കുന്നു. ഉദ്ദേശിക്കുന്നത് പോലെ ഈ പ്രവർത്തി നടക്കണം എന്നില്ല. + + നിശ്ശബ്ദമാക്കാനുള്ള സമയം ഒരു മിനുട്ടിൽ കൂടുതൽ വേണം. + %2$s വരെ %1$s നെ നിശ്ശബ്ദമാക്കിയിട്ടുണ്ട്. \n വേണമെങ്കിൽ ഈ അക്കൗണ്ടിനെ അവരുടെ പ്രൊഫൈൽ പേജിൽ നിന്ന് പഴയ പടി ആക്കാം. + %2$s വരെ %1$s നെ നിശ്ശബ്ദമാക്കിയിട്ടുണ്ട്. \n പഴയപടി ആക്കാൻ ഇവിടെ ഞെക്കൂ. + + പ്രദർശിപ്പിക്കാൻ അറിയിപ്പുകൾ ഇല്ല + താങ്കളെ പ്രതിപാദിച്ചിരിക്കുന്നു + wrote a new message + തങ്ങളുടെ സ്റ്റാറ്റസ് ബൂസ്റ്റ് ചെയ്യപ്പെട്ടിരിക്കുന്നു + താങ്കളുടെ സ്റ്റാറ്റസ് പ്രിയപെട്ടവയിൽ ചേർത്തിരിക്കുന്നു + നിങ്ങളെ പിന്തുടരുന്നു + പിന്തുടരാൻ ആവശ്യപ്പെട്ടു + + മറ്റൊരു അറിയിപ്പ് + and %d other notifications + + + %d ഇഷ്ടം + %d ഇഷ്ടം + + ഒരു അറിയിപ്പ് നീക്കം ചെയ്യട്ടെ? + എല്ലാ അറിയിപ്പുകളും നീക്കം ചെയ്യട്ടെ? + അറിയിപ്പ് നീക്കം ചെയ്തിരിക്കുന്നു! + എല്ലാ അറിയിപ്പുകളും നീക്കം ചെയ്തിരിക്കുന്നു! + + പിന്തുടരുന്നു + അനുയായികൾ + പിൻ കുത്തി വച്ചിരിക്കുന്നു + + ഉപയോക്താവിന്റെ ഐ ഡി എടുക്കാൻ സാധ്യമല്ല! + ഈ പതിപ്പിന്റെ ഡൊമെയിനിലേക്ക് ബന്ധപ്പെടുവാൻ സാധിക്കുന്നില്ല! + ഇന്റർനെറ്റ് കണക്ഷൻ ഇല്ല! + ഈ അക്കൗണ്ട് തടഞ്ഞിരുന്നു! + ഈ അക്കൗണ്ട് ഇനിമുതൽ തടയപ്പെടുന്നതല്ല! + ഈ അക്കൗണ്ട് നിശ്ശബ്ദമാക്കിയിരുന്നു! + ഈ അക്കൗണ്ട് ഇനിമുതൽ നിശ്ശബ്ദമാക്കപ്പെടുന്നതല്ല! + ഈ അക്കൗണ്ട് പിന്തുടരപ്പെട്ടിരുന്നു! + ഈ അക്കൗണ്ട് ഇനിമുതൽ പിന്തുടരപ്പെടുന്നില്ല! + ഈ ടൂട്ടിനെ ബൂസ്റ്റ് ചെയ്തിരിക്കുന്നു! + ഈ ടൂട്ട് ബൂസ്റ്റിൽ നിന്ന് മാറ്റിയിരിക്കുന്നു! + ഈ ടൂട്ടിനെ പ്രിയപ്പെട്ടവയിലേക്ക് ചേർത്തിരിക്കുന്നു! + ഈ ടൂട്ടിനെ പ്രിയപ്പെട്ടവയിൽ നിന്ന് മാറ്റിയിരിക്കുന്നു! + ഈ ടൂട്ടിനെ റിപ്പോർട് ചെയ്തിരിക്കുന്നു! + ഈ ടൂട്ട് നീക്കം ചെയ്യപ്പെട്ടിരിക്കുന്നു! + ഈ ടൂട്ട് പിൻ ചെയ്തിരിക്കുന്നു! + ഈ ടൂട്ട് പിൻ നീക്കം ചെയ്തിരിക്കുന്നു! + അയ്യോ! ഒരു പിഴവ് സംഭവിച്ചു! + ഒരു പിഴവ് സംഭവിച്ചു! ഈ പതിപ്പ് അടയാളവാക്യം തിരികെ തന്നില്ല! + ഈ പതിപ്പിന്റെ മണ്ഡലം സാധുവല്ല! + അക്കൗണ്ടുകൾ മാറ്റുമ്പോൾ ഒരു പിഴവ് സംഭവിച്ചു! + തിരയുമ്പോൾ ഒരു പിഴവ് സംഭവിച്ചു! + ഈ പ്രൊഫൈലിലെ വിവരങ്ങൾ സൂക്ഷിക്കപ്പെട്ടിരിക്കുന്നു! + ഒരു നടപടിയും എടുക്കാൻ സാധ്യമല്ല + മാധ്യമം സൂക്ഷിക്കപ്പെട്ടിരിക്കുന്നു! + പരിഭാഷപ്പെടുത്തുമ്പോള്‍ ഒരു പിഴവ് സംഭവിച്ചു! + പരിഭാഷപ്പെടുത്തൽ ക്രമീകരണങ്ങളിൽ അപ്രാപ്തമാക്കിയിരിയ്ക്കുന്നു + കരട് രക്ഷിച്ചു! + ഈ സേർവർ ഇത്രയധികം അക്ഷരങ്ങള്‍ അനുവധിയ്ക്കുമെന്നു് നിങ്ങള്‍ക്കുറപ്പാണോ? സാധാരണ ഈ പരിധി 500 അക്ഷരങ്ങളാണു്. + %1$s ന്റെ ടൂട്ടുകളുടെ ദൃശ്യപരതയിൽ മാറ്റം വരുത്തി + + ഓരോ തവണയും ലോഡു് ചെയ്യേണ്ട ടൂട്ടുകളുടെ എണ്ണം + എല്ലായിപ്പോഴും + വൈഫൈയിൽ മാത്രം + എപ്പോഴും ചോദിക്കുക + മീഡിയ ലോഡുചെയ്യുക + ചിത്രങ്ങള്‍ ലോഡുചെയ്യുക + കൂടുതൽ കാണിക്കുക… + കുറച്ചു കാണിക്കുക… + അസ്വസ്ഥമാക്കാമാകുന്ന ഉള്ളടക്കം + ജിഫ് അവതാരങ്ങൾ പ്രവർത്തനരഹിതമാക്കുക + വഴി: + ഡ്രാഫ്റ്റുകൾ സ്വയമേ സൂക്ഷിക്കുക + ടൂട്ടുകളിൽ മീഡിയയുടെ URL ചേർക്കുക + ആരെങ്കിലും നിങ്ങളെ പിന്തുടരുമ്പോൾ അറിയിക്കുക + ആരെങ്കിലും നിങ്ങളുടെ സ്റ്റാറ്റസ് ബൂസ്റ്റു് ചെയ്താൽ അറിയിക്കുക + ആരെങ്കിലും നിങ്ങളുടെ സ്റ്റാറ്റസ് പ്രിയപ്പെട്ടതാക്കിയാൽ അറിയിക്കുക + ആരെങ്കിലും നിങ്ങളെ പരാമർശിച്ചാൽ അറിയിക്കുക + വോട്ടെടുപ്പു് അവസാനിച്ചാൽ അറിയിക്കുക + Notify for new posts + ബൂസ്റ്റുചെയ്യുന്നതിനു് മുന്പു് സ്ഥിരീകരിയ്ക്കുക + പ്രിയങ്കരങ്ങളിലേക്ക് ചേർക്കുന്നതിന് മുമ്പ് സ്ഥിരീകരണ ഡയലോഗ് കാണിക്കുക + വൈഫൈയിൽ മാത്രം അറിയിപ്പുകള്‍ കാണിയ്ക്കുക + അറിയിക്കുക? + നിശബ്‌ദ അറിയിപ്പുകൾ + NSFW view timeout (seconds, 0 means off) + Media Description timeout (seconds, 0 means off) + പ്രൊഫൈൽ തിരുത്തുക + ഇച്ഛാനുസൃതമായി പങ്കുവയ്ക്കുക + നിങ്ങളുടെ ഇഷ്‌ടാനുസൃത പങ്കിടൽ URL… + ബയോ… + അക്കൗണ്ട് ലോക്കു് ചെയ്യുക + മാറ്റങ്ങൾ സംരക്ഷിക്കുക + ഒരു തലക്കെട്ട് ചിത്രം തിരഞ്ഞെടുക്കുക + Fit preview images + Automatically split toots in replies when chars are over: + You have reached the 160 characters allowed! + താങ്കൾ അനുവദനീയമായ 30 അക്ഷരങ്ങളുടെ പരിധി എത്തിയിരിക്കുന്നു! + തമ്മിൽ + കൂടാതെ + സമയം %1$s ൽ കൂടുതലായിരിക്കണം + സമയം %1$s ൽ കുറവായിരിക്കണം + ആരംഭിക്കുന്ന സമയം + End time + Use the built-in browser + Custom tabs + Enable Javascript + Automatically expand cw + Allow third-party cookies + Your API key, you can leave blank for Yandex + + Dark + Light + Black + + Set LED colour: + + Blue + Cyan + Magenta + Green + Red + Yellow + White + + Follow + Unblock + Mute + Unmute + Request sent + Follows you + Search + First letter in capital for replies + Resize pictures + Resize videos + + Push notifications + Please, confirm push notifications that you want to receive. + You can enable or disable these notifications later in settings (Notifications tab). + + + Clear cache + There are %1$s of data in cache.\n\nWould you like to delete them? + Mb + Cache was cleared! %1$s were released + + Title + Title… + Description + Keywords + Keywords… + + Synchronize + Filter + Your toots + Your notifications + Public + Unlisted + Private + നേരിട്ടു് + ചില കീവേഡുകൾ… + മീഡിയ കാണിക്കുക + പിൻ ചെയ്തതു് കാണിയ്ക്കുക + പൊരുത്തപ്പെടുന്ന ഫലങ്ങളൊന്നും കണ്ടെത്തിയില്ല! + Backup toots for %1$s + %1$s new toots have been imported + %1$s new notifications have been imported + + തീയതി അവരോഹണത്തിൽ + തീയതി ആരോഹണത്തിൽ + + + വേണ്ട + മാത്രം + എല്ലാം + + No toots were found in database. Please, use the synchronize button from the menu to retrieve them. + + Recorded data + Only basic information from accounts are stored on the device. + These data are strictly confidential and can only be used by the application. + Deleting the application immediately removes these data.\n + ⚠ Login and passwords are never stored. They are only used during a secure authentication (SSL) with an instance. + + Permissions: + - ACCESS_NETWORK_STATE: Used to detect if the device is connected to a WIFI network.\n + - INTERNET: Used for queries to an instance.\n + - WRITE_EXTERNAL_STORAGE: Used to store media or to move the app on a SD card.\n + - READ_EXTERNAL_STORAGE: Used to add media to toots.\n + - BOOT_COMPLETED: Used to start the notification service.\n + - WAKE_LOCK: Used during the notification service. + + API permissions: + - Read: Read data.\n + - Write: Post statuses and upload media for statuses.\n + - Follow: Follow, unfollow, block, unblock.\n\n + ⚠ These actions are carried out only when user requests them. + + Tracking and Libraries + The application does not use tracking tools (audience measurement, error reporting, etc.) and does not contain any advertising.\n\n + The use of libraries is minimized: \n + - Glide: To manage media\n + - Android-Job: To manage services\n + - PhotoView: To manage images\n + + Translation of toots + The application offers the ability to translate toots using the locale of the device and the Yandex API.\n + Yandex has its proper privacy-policy which can be found here: https://yandex.ru/legal/confidential/?lang=en + + Thank you to: + + Filter out by regular expressions + Search + Delete + Fetch more toots… + + Lists + Are you sure you want to permanently delete this list? + There is nothing in this list yet. When members of this list post new statuses, they will appear here. + Add to list + Add list + Delete list + Edit list + New list title + The account was added to the list! + You don\'t have any lists yet! + + %1$s has moved to %2$s + Authentication does not work? + Here are some checks that might help:\n\n + - Check there is no spelling mistakes in the instance name\n\n + - Check that your instance is not down\n\n + - If you use the two-factor authentication (2FA), please use the link at the bottom (once the instance name is filled)\n\n + - You can also use this link without using the 2FA\n\n + - If it still does not work, please raise an issue on FramaGit at https://framagit.org/tom79/fedilab/issues + + Media has been loaded. Tap here to display it. + This action can be quite long. You will be notified when it will be finished. + Still running, please wait… + Export statuses + Export statuses for %1$s + %1$s toots out of %2$s have been exported. + Something went wrong when exporting data for %1$s + Something went wrong when exporting data! + Something went wrong when importing data! + + Proxy + Enable proxy? + Host + Port + Login + Password + Add toot details when sharing + Support the app on Liberapay + There is an error in the regular expression! + No timelines was found on this instance! + Delete this instance? + Translate in + Follow instance + You already follow this instance! + The instance is followed! + Partnerships + Information + Hide boosts from %s + Feature on profile + Show boosts from %s + Don\'t feature on profile + The account is now featured on profile + The account is no longer featured on profile + Boosts are now shown! + Boosts are now hidden! + Direct message + Filters + No filters to display. You can create one by tapping on the \"+\" button. + Keyword or phrase + Home timeline + Public timelines + Notifications + Conversations + Will be matched regardless of casing in text or content warning of a toot + Drop instead of hide + Filtered toots will disappear irreversibly, even if filter is later removed + When the keyword or phrase is alphanumeric only, it will only be applied if it matches the whole word + Whole word + Filter contexts + One or multiple contexts where the filter should apply + Expire after + Delete filter? + Update filter + Create filter + Whom to follow + There is no accounts listed for the moment! + Follow + Select all + Unselect all + %s is followed! + Creating the list %s + Adding accounts to the list + Accounts were added to the list + Adding accounts to the list + You have not created a list yet. Tap on the \"+\" button to add a new one. + Who to follow + Trunk API + Account(s) can\'t be followed + Fetching remote account + Automatically expand hidden media + New follow + New Boost + New Favourite + New Mention + Poll Ended + New Toot + Toots Backup + New posts + Media Download + Change notification sound + Select Tone + Enable time slot + How To Videos + Fetching remote thread! + No blocked domains! + Unblock domain + Are you sure to unblock %s? + Are you sure to block %s?\n\nYou will not see any content from that domain in any public timeline or in your notifications. Your followers from that domain will be removed. + Blocked domains + Block domain + The domain is blocked + The domain is no longer blocked! + Fetching remote status + Comment + Peertube instance + Be the first to leave a comment on this video with the top right button! + %s views + Duration: %s + Add an instance + Comments are not enabled on this video! + Pick up a resolution + Peertube favourites + The video has been added to bookmarks! + The video has been removed from bookmarks! + There is no Peertube videos in your favourites! + Channel + Videos + Channels + Use Emoji One + Information + Display previews in all toots + New UX/UI designer + Display video previews + The account id has been copied in the clipboard! + Change the language + Default language + Truncate long toots + Truncate toots over \'x\' lines. Zero means disabled. + Display more + Display less + Manage tags + The tag already exists! + The tag has been stored! + The tag has been changed! + The tag has been removed! + Schedule boost + The boost is scheduled! + No scheduled boost to display! + Schedule boost.]]> + Art timeline + Open menu + Go back + Logo of the application + Profile picture + Profile banner + Contact admin of the instance + Add new + MastoHost logo + Emoji picker + Refresh + Expand the conversation + Remove an account + Remove the blocked domain + Custom emoji picker + Play video + New toot + Image of the card + Hide media + Favicon + Add description for media (for the visually impaired) + + Never + 30 minutes + 1 hour + 6 hours + 12 hours + 1 day + 1 week + + In this field, you need to write your instance domain.\nFor example, if you created your account on https://mastodon.social\nJust write mastodon.social\nYou can start writing first letters and names will be suggested. + + More information + + Languages + Media only + Show NSFW + Crowdin translations + Crowdin manager + Translation of the application + About Crowdin + Bot + Pixelfed instance + Mastodon instance + Any of these + All of these + None of these + Any of these words (space-separated) + All these words (space-separated) + Add some words to filter (space-separated) + Change column name + Misskey instance + No app supporting this link is installed on your device. + Subscriptions + Overview + Trending + Recently added + Local + Upload + Reply + Delete a comment + Are you sure to delete this comment? + Full screen video + Mode for videos + Select the file to upload + My videos + Title + License + Category + Language + This video contains mature or explicit content + Enable video comments + Update video + Description + The video has been updated! + Upload cancelled! + The video has been uploaded! + Uploading, please wait… + Tap here to edit the video data. + Delete video + Are you sure to delete this video? + Display NSFW videos + No videos to display! + Leave a comment + Share + Choose a schedule mode + From device + From server + Toots (Server) + Toots (Device) + Modify + Display new toots above the \"Fetch more\" button + Timelines + Interface + Contacts + %1$s commented your video %2$s]]> + %1$s is following your channel %2$s]]> + %1$s is following your account]]> + %1$s has been published]]> + %1$s succeeded]]> + %1$s failed]]> + %1$s published a new video: %2$s]]> + %1$s has been blacklisted]]> + %1$s has been unblacklisted]]> + Export data + Import Data + Select the file to import + An error occurred when selecting the backup file! + Add a public comment + Send comment + There is no Internet connection. Your message has been stored in drafts. + Plain text + HTML + Markdown + Logout account + All + Support the app + Open Collective enables groups to quickly set up a collective, raise funds and manage them transparently. + Copy link + Connect + Normal + Compact + Console + Set display mode + Patch the Security Provider + Update tracking domains + The tracking data base has been updated! + http calls blocked by the application + List of blocked calls + Submit + The data base has been exported! + Featured hashtags + Filter timeline with tags + No tags + Hide the \"delete\" button in the notification tab + Attach an image when sharing a URL + + Poll + Polls + Create a poll + Choice 1 + Choice 2 + Choice %d + You need two choices at least for the poll! + Done + end at %s + Refresh poll + Vote + A poll you have voted in has ended + A poll you tooted has ended + Customize + Categories + Time slot + Advanced + Display \'new\' badge on unread toots + Peertube + Move timeline + Hide timeline + Reorder timelines + List permanently deleted + Followed instance removed + Pinned tag removed + Undo + You need to keep two visible tabs! + Reorder timelines + Main timelines can only be hidden! + BBCode + Always mark media as sensitive + GNU instance + Cached status + Forward tags in replies + Long press to store media + Blur sensitive media + Display timelines in a list + Display timelines + Mark bot accounts in toots + Manage tags + Remember the position in Home timeline + History + Playlists + Display name + You don\'t have any playlists. Tap on the \"+\" icon to add a new playlist + You must provide a display name! + The channel is required when the playlist is public. + Create a playlist + There is nothing in this playlist yet. + redo + Gallery + Emoji + Sticker + Eraser + Text + Filter + Brush + Are you sure you want to exit without saving the image? + Discard + Saving… + Image Saved Successfully! + Failed to save Image + Opacity + Enable photo editor + Add a poll item + Remove last poll item + Mute conversation + Unmute conversation + The conversation is no longer muted! + The conversation is muted + Open application features + Timed mute + Mention the account + Refresh cache + Mention the status + News + General + Regional + Art + Journalism + Activism + Gaming + Technology + Adult content + Furry + Food + Logo of the instance + Something went wrong when checking available instances! + Join Mastodon + Choose an instance by picking up a category, then tap on a check button. + Choose an instance by tapping on a check button. + %1$s users + Confirm password + I agree to %1$s and %2$s + server rules + terms of service + Sign up + This instance works with invitations. Your account will need to be manually approved by an administrator before being usable. + Please, fill all the fields! + Passwords don\'t match! + The email doesn\'t seem to be valid! + Your username will be unique on %1$s + You will be sent a confirmation e-mail + Use at least 8 characters + Password should contain at least 8 characters + Username should only contain letters, numbers and underscores + Account created! + Your account has been created!\n\n + Think to validate your email within the 48 next hours.\n\n + You can now connect your account by writing %1$s in the first field and tap on Connect.\n\n + Important: If your instance required validation, you will receive an email once it is validated! + + Save the message in drafts? + Administration + Reports + No reports to display! + Reconnect the account + The application failed to access the admin features. You might need to reconnect the account for having the correct scope. + Unresolved + Remote + Active + Pending + Disabled + Silenced + Suspended + Permissions + Email status + Login status + Joined + Most recent IP + Warn + Disable + Silence + Notify the user per e-mail + Custom warning + User + Moderator + Administrator + Confirmed + Not confirmed + Reported statuses + Account + Undo silence + Undo disable + Suspend + Undo suspend + The account is silenced! + The account is no longer silenced! + The account is suspended! + The account is no longer suspended! + The account is disabled! + The account is no longer disabled! + The account has been warned! + Display the admin menu + Display the admin feature in statuses + Allow + The account is approved! + The account is rejected! + Assign to me + Unassign + Mark as resolved + Mark as unresolved + Empty content! + Display Fedilab features button + The application needs to access audio recording + Voice message + Enable quick reply + The account you are replying might not see your message! + If disabled, the app will always load last statuses + If disabled, sensitive media will be hidden with a button + Store media in full size with a long press on previews + Add an ellipse button at the top right for listing all tags/instances/lists + During the time slot, the app will send notifications. You can reverse (ie: silent) this time slot with the right spinner. + Display a Fedilab button below profile picture. It is a shortcut for accessing in-app features. + Allow to reply directly in timelines below statuses + Previews will not be cropped in timelines + Allow to play embedded videos directly in timelines + Allow to reverse the way to read statuses that are displayed once tapping the fetch more button + This option allows to support recent cipher suites. It is useful for older Android devices or if you cannot connect to your instance. + Exclusively for Peertube videos. Switch this mode if you cannot play them. + These tags will allow to filter statuses from profiles. You will have to use the context menu for seeing them. + Automatically insert a line break after the mention to capitalize the first letter + Allow content creators to share statuses to their RSS feeds + Compose + Maximum retry times when uploading media + Create a new Folder here + Enter the folder name + Please enter a valid folder name + This folder already exists.\n Please provide another name for the folder + Select + Default Directory + Folder + Create folder + Display a toast message after an action has been completed (boost, fav, etc.)? + Muted instances have been exported! + Add an instance + Export instances + Import instances + Crash reports + Enable crash reports + If enabled, a crash report will be created locally and then you will be able to share it. + Fedilab has stopped :( + You can send me by email the crash report. It will help to fix it :)\n\nYou can add additional content. Thank you! + Use the wysiwyg + When enabled, you will be able to format your text easily with tools. + Statistics + Total statuses + Number of boosts + Number of favourites + Number of mentions + Number of follows + Number of polls + Number of replies + Number of statuses + Statuses + Visibility + Number with media + Number with sensitive media + Number with CW + First status date + Last status date + First notification date + Last notification date + Frequency + %s statuses per day + %s notifications per day + Date range + Groups + No groups! + Disable custom animated emojis + Charts + Display charts + The application collects your local data, please wait… + Backup + Auto backup statuses + This option is per account. It will launch a service that will automatically store your statuses locally in the database. That allows to get statistics and charts + Auto backup notifications + This option is per account. It will launch a service that will automatically store your notifications locally in the database. That allows to get statistics and charts + Report account + Send an invitation + Your instance does not allow to register a new account! + + %d vote + %d votes + + + %d voter + %d voters + + + Single choice + Multiple choices + + + 5 minutes + 30 minutes + 1 hour + 6 hours + 1 day + 3 days + 7 days + + + Webview + Direct stream + + For joining my instance \"%1$s\", you can download Fedilab:\n\nF-Droid: %2$s\nGoogle: %3$s\n\nThen open the link below with Fedilab and create your account :)\n\n%4$s + + Your poll can\'t have duplicated options! + For all accounts + Database cache + Clear your home timeline cache + Clear your cached statuses + Clear your bookmarks + Files in cache + Total notifications + Hide menu items + Fedilab is running live notifications + For %1$s accounts with %2$s events + Live notifications for %1$s + Live notifications will be enabled for this account. + Clear cache when leaving + The cache (media, cached messages, data from the built-in browser) will be automatically cleared when leaving the application. + Do you want to unfollow this account? + Show confirmation dialog before unfollowing + Replace Youtube with Invidio.us + Invidious is an alternative front-end to YouTube + Enter your custom host or leave blank for using invidious.snopyta.org + Replace Twitter with Nitter + Nitter is an open source alternative Twitter front-end focused on privacy. + Enter your custom host or leave blank for using nitter.net + Replace Instagram with Bibliogram + Bibliogram is an open source alternative Instagram front-end focused on privacy. + Enter your custom host or leave blank for using bibliogram.art + Replace Reddit with Libreddit + Libreddit is an open source alternative Reddit front-end focused on privacy. + Enter your custom host or leave blank for using libredd.it + Replace Medium links + Replace medium.com links with an open source alternative front-end focused on privacy. + Default: scribe.rip + Replace Wikipedia links + Replace Wikipedia link with an open source alternative front-end focused on privacy. + Default: wikiless.org + Hide Fedilab notification bar + For hiding the remaining notification in the status bar, tap on the eye icon button then uncheck: \"Display in status bar\" + Use a push notifications system for getting notifications in real time. + No live notifications + Live notifications + Notifications will be fetched every 15 minutes. + Add notes + Notes for the account + Allow to compress large photos into smaller sized photos with very less or negligible loss in quality of the image. + Allow compressing videos while maintaining their quality. + The app is compressing the media, it can be quite long… + Change app icon + Tap to change the app icon + Post + Visibility of the post + Tap here to add photos + Accepted Formats: jpeg, png, gif \n\nMax File Size: 15 MB \n\nAlbums can contain up to 4 photos or videos + Upload media + Add an optional caption + The app received a very long error message from the API %1$s + Message preview + Add mentions in each message + Fetching conversation + Order by + Title for the video + Join Peertube + I am at least 16 years old and agree to the %1$s of this instance + Links + Change the color of links (URLs, mentions, tags, etc.) in messages + Reblogs header + Change the color of display name at the top of messages + Change the color of the user name at the top of messages + Change the color of the header for reblogs + Posts + Background color of posts in timelines + Reset colors + Tap here to reset all your custom colors + Reset + Icons + Color of bottom icons in timelines + Pin this tag + Logo of the instance + Edit profile + Make an action + Translation + Image preview + Text color + Change the text color in pots + Apply changes + You need to restart the application to apply changes + Restart + Use a custom theme + Allow to override colors of the selected theme above + Theming + Store before + The theme was exported + The theme has been successfully exported in CSV + Apply the primary color to the status bar + Status bar color + Restore a default theme + Import a theme + Tap here to import a theme from a previous export + Export the theme + Tap here to export the current theme + An error occurred when selecting the theme file + Theme Picker + Select a pre-installed theme + Themes + Apply the primary color to the navigation bar + Navigation bar color + The underlying color of the app’s content. + Background color + Accents select parts of the UI. + Accent color + Displayed most frequently across your app. + Primary color + Export bookmarks to the instance + Import bookmarks from the instance + User count + Status count + Instance count + Blocked + End in %s + What\'s new in %s + You can follow my account for updates + This instance is not available on https://instances.social + Display full link + Share link + The URL has been copied to the clipboard + Open with another app + Check redirect + This URL does not redirect + %1$s \n\nredirects to\n\n %2$s + Change the user agent + Set a custom user agent or leave blank + Allows to customize the user agent used for api calls or with the built-in browser. + Remove UTM parameters + The app will automatically remove UTM parameters from URLs before visiting a link. + Trends + Trending now + %d people talking + Twitter accounts (via Nitter) + Twitter usernames space separated + Identity proofs + Verified identity + Verified by %1$s (%2$s) + Delete the notification + Display more options + It is a Pixelfed story + Upload a media, it will be automatically added to your Pixelfed story. + Media successfully added to your story! + Action disabled + Unfollow + Something went wrong, please check your download directory in settings. + Announcements + No announcements! + Add a reaction + Use your favourite browser inside the app. Uncheck this feature to open links externally. + Video cache in MB, zero means no cache. + Watermarks + Automatically add a watermark at the bottom of pictures. The text can be customized for each account. + No distributors found! + You need a distributor for receiving push notifications.\nYou will find more details at %1$s.\n\nYou can also disable push notifications in settings for ignoring that message. + Select a distributor + diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml new file mode 100644 index 000000000..9a2696f2b --- /dev/null +++ b/app/src/main/res/values-nl/strings.xml @@ -0,0 +1,1141 @@ + + + Menu openen + Menu sluiten + Over + Over deze server + Privacy + Buffer + Uitloggen + Inloggen + + Sluiten + Ja + Nee + Annuleren + Downloaden + %1$s downloaden + Media opgeslagen + Bestand: %1$s + Wachtwoord + E-mail + Accounts + Toots + Tags + Opslaan + Herstellen + Geen resultaten! + Server + Server: bv. mastodon.social + Werkt nu met account %1$s + Account toevoegen + De inhoud van deze toot is naar het klembord gekopieerd + De link van de toot is naar het klembord gekopieerd + Veranderen + Kies een afbeelding… + Leegmaken + Camera + Alles verwijderen + Deze toot vertalen. + Inplannen + Tekst- en pictogramgrootte + De huidige tekstgrootte veranderen: + De huidige pictogramgrootte veranderen: + Volgende + Vorige + Open met + Bevestigen + Media + Delen met + Gedeeld via Fedilab + Reacties + Gebruikersnaam + Concepten + Favorieten + Nieuwe volgers + Meldingen + Boosts + Boosts tonen + Reacties tonen + In webbrowser openen + Vertalen + Wacht hier een paar seconden mee. + + Start + Lokale tijdlijn + Globale tijdlijn + Opties + Favorieten + Communicatie + Genegeerde gebruikers + Geblokkeerde gebruikers + Meldingen + Volgverzoeken + Instellingen + Account verwijderen + Account %1$s uit deze app verwijderen? + E-mail verzenden + Op het pad klikken om het te veranderen + Mislukt! + Ingeplande toots + De informatie hieronder kan een incompleet beeld geven van dit gebruikersprofiel. + Emoji toevoegen + De app heeft nog geen lokale emojis verzameld. + Push notifications + Weet je zeker dat je wilt uitloggen? + Are you sure you want to logout @%1$s@%2$s? + + Geen toot om weer te geven + Geen verhalen om te laten zien + Verhalen + Geboost door %1$s + Deze toot aan jouw favorieten toevoegen? + Deze toot uit jouw favorieten verwijderen? + Deze toot boosten? + Deze toot niet langer boosten? + Deze toot aan jouw profielpagina vastmaken? + Deze toot van profielpagina losmaken? + Negeren + Blokkeren + Rapporteren + Verwijderen + Kopiëren + Delen + Vermelden + Tijdelijk negeren + Verwijderen en herschrijven + + Account negeren? + Account blokkeren? + Toot rapporteren? + Dit domein blokkeren? + Dit account niet langer negeren? + Deze rekening deblokkeren? + + + Melden + Stil + + + Toot verwijderen? + Deze toot verwijderen en herschrijven? + + Bladwijzers + Aan bladwijzers toevoegen + Bladwijzer verwijderen + Geen bladwijzers! + Toot is aan jouw bladwijzers toegevoegd! + Toot is uit jouw bladwijzers verwijderd! + + %d s + %d m + %d u + %d d + + %d second + %d seconds + + + %d minute + %d minutes + + + %d hour + %d hours + + + %d day + %d days + + + Waarschuwing + Wat wil je kwijt? + TOOT! + QUEET! + cw + Schrijf een toot + Reageer op een toot + Schrijf een queet + Op een queet reageren + Kies een afbeelding of video + Tijdens het kiezen van een afbeelding of video heeft zich een fout voorgedaan! + Media verwijderen? + Jouw toot is leeg! + Zichtbaarheid van toot + Standaard zichtbaarheid van toot: + De toot is verzonden! + Je reageert op deze toot: + Gevoelige inhoud? + + Op openbare tijdlijnen tonen + Niet op openbare tijdlijnen tonen + Alleen aan volgers tonen + Alleen aan vermelde gebruikers tonen + + Geen concepten! + Kies een toot + Kies een account + Kies enkele accounts + Concept verwijderen? + Klik om de oorspronkelijke toot te tonen + Omschrijf dit voor mensen met een visuele beperking + + Geen beschrijving beschikbaar! + + Release %1$s + Ontwikkelaar: + Licentie: + GNU GPL V3 + Broncode: + Vertaling van toots: + Servers zoeken: + Ontwerper pictogram: + + Conversatie + + Geen account om weer te geven + Geen volgverzoeken + Toots \n %1$s + Volgt \n %1$s + Volgers \n %1$s + Vastgezet \n %d + Goedkeuren + Afkeuren + + Geen ingeplande toots! + Schrijf een toot en kies dan Inplannen in het menu rechtsboven. + Ingeplande toot verwijderen? + Media: %d + De toot is ingepland! + De toot moet wel in de toekomst ingepland worden! + Energiebesparing is ingeschakeld! Het inplannen werkt mogelijk niet zoals verwacht. + + De tijd om te negeren moet langer duren dan een minuut. + %1$s wordt tot %2$s genegeerd.\n Ga naar het profiel van dit account om het negeren op te heffen. + %1$s wordt tot %2$s genegeerd.\n Klik hier om dit account niet langer te negeren. + + Je hebt nog geen meldingen + vermeldde jou + wrote a new message + boostte jouw toot + markeerde jouw toot als favoriet + volgt jou nu + gevraagd om u te volgen + + en één andere melding + en %d andere meldingen + + + %d vind-ik-leuk + %d vinden dit leuk + + Deze melding verwijderen? + Alle meldingen verwijderen? + De melding is verwijderd! + Alle meldingen zijn verwijderd! + + Volgt + Volgers + Vastgezet + + Niet in staat om client-id te verkrijgen! + Niet in staat om verbinding te maken met het instance-domein! + Geen internetverbinding! + Account is geblokkeerd! + Account is gedeblokkeerd! + Account wordt nu genegeerd! + Account wordt niet langer genegeerd! + Account wordt nu gevolgd! + Account wordt niet langer gevolgd! + Toot is geboost! + Toot wordt niet langer geboost! + Toot is als favoriet gemarkeerd! + Toot wordt niet langer als favoriet gemarkeerd! + Toot is gerapporteerd! + Toot is verwijderd! + Toot is vastgezet! + Toot is losgemaakt! + Oeps! Er ging wat mis! + Er ging wat mis! De server gaf geen autorisatiecode terug! + De domeinnaam van de server is onjuist! + Er ging wat mis tijdens het omschakelen van accounts! + Er ging wat mis tijdens het zoeken! + De profielgegevens zijn opgeslagen! + Er valt niets te doen + Media is opgeslagen! + Er ging wat mis tijdens het vertalen! + Vertalingen zijn uitgeschakeld in instellingen + Concept opgeslagen! + Weet je zeker dat je op deze server zoveel tekens mag gebruiken? Standaard is er een limiet van 500 karakters. + Zichtbaarheid toots van account %1$s is veranderd + + Aantal toots per keer + Altijd + Wifi + Vragen + Media laden + Laad de afbeeldingen of video + Meer tonen + Minder tonen + Gevoelige inhoud + GIF-avatars uitschakelen + Downloadlocatie: + Concepten automatisch opslaan + URL van afbeelding of video aan toot toevoegen + Geef een melding wanneer iemand jou volgt + Geef een melding wanneer jouw toot is geboost + Geef een melding wanneer jouw toot als favoriet is gemarkeerd + Geef een melding wanneer iemand jou vermeldt + Geef een melding wanneer een poll is beëindigd + Notify for new posts + Vraag voor het boosten een bevestiging + Vraag voor het markeren als favoriet een bevestiging + Alleen met Wifi meldingen tonen + Meldingen? + Stille meldingen + Hoe lang gevoelige inhoud blijven tonen (in seconden, 0 is onbeperkt) + Media Omschrijving time-out (seconden, 0 betekent uit) + Profiel bewerken + Handmatig delen + Jouw handmatige deel-URL… + Bio… + Account vergrendelen + Wijzigingen opslaan + Omslagfoto kiezen + Voorbeeldafbeeldingen passend maken + Toots met meer dan 500 tekens automatisch als reacties opsplitsen + Je hebt het limiet van 160 karakters bereikt! + Je hebt het limiet van 30 karakters bereikt! + Tussen + en + Dit tijdstip moet na %1$s zijn + Dit tijdstip moet voor %1$s zijn + Begintijd + Eindtijd + Gebruik de ingebouwde webbrowser + Aangepaste tabs + Javascript toegestaan + Automatisch tekstwaarschuwingen (cw) uitklappen + Cookies van derden accepteren + Je hoeft geen API-sleutel voor Yandex te gebruiken + + Donker + Licht + Zwart + + LED-kleur instellen: + + Blauw + Cyaan + Magenta + Groen + Rood + Geel + Wit + + Volgen + Deblokkeren + Negeren + Niet langer negeren + Verzoek verzonden + Volgt jou + Zoeken + Eerste letter een hoofdletter in reacties + Afbeeldingen schalen + Resize video\'s + + Pushmeldingen + Bevestig de pushmeldingen die jij wilt ontvangen. + Je kan deze later in- en uitschakelen in jouw instellingen (onder meldingen). + + + Buffer leegmaken + De buffer bevat %1$s aan gegevens.\n\nWil je dit verwijderen? + Mb + De buffer is leeggemaakt! Er is %1$s vrijgekomen + + Titel + Titel… + Omschrijving + Trefwoorden + Trefwoorden… + + Synchroniseren + Filter + Jouw toots + Uw meldingen + Openbaar + Minder openbaar + Alleen volgers + Direct + Enkele trefwoorden… + Media tonen + Vastgezette toots tonen + Geen resultaten! + Toots back-uppen voor %1$s + Er zijn %1$s nieuwe toots geïmporteerd + %1$s nieuwe kennisgevingen zijn ingevoerd + + Data aflopend + Data oplopend + + + Nee + Alleen + Allebei + + Er zijn geen toots in de database gevonden. Gebruik de synchronisatieknop om deze op te halen. + + Recorded data: + Only basic information from accounts are stored on the device. + These data are strictly confidential and can only be used by the application. + Deleting the application immediately removes these data.\n + ⚠ Login and passwords are never stored. They are only used during a secure authentication (SSL) with an instance. + + Permissions: + - ACCESS_NETWORK_STATE: Used to detect if the device is connected to a WIFI network.\n + - INTERNET: Used for queries to an instance.\n + - WRITE_EXTERNAL_STORAGE: Used to store media or to move the app on a SD card.\n + - READ_EXTERNAL_STORAGE: Used to add media to toots.\n + - BOOT_COMPLETED: Used to start the notification service.\n + - WAKE_LOCK: Used during the notification service. + + API permissions: + - Read: Read data.\n + - Write: Post statuses and upload media for statuses.\n + - Follow: Follow, unfollow, block, unblock.\n\n + ⚠ These actions are carried out only when user requests them. + + Tracking and Libraries: + The application does not use tracking tools (audience measurement, error reporting, etc.) and does not contain any advertising.\n\n + The use of libraries is minimized: \n + - Glide: To manage media\n + - Android-Job: To manage services\n + - PhotoView: To manage images\n + + Translation of toots + The application offers the ability to translate toots using the locale of the device and the Yandex API.\n + Yandex has its proper privacy-policy which can be found here: https://yandex.ru/legal/confidential/?lang=en + + Met dank aan: + + Wegfilteren met reguliere expressies + Zoeken + Verwijderen + Meer toots ophalen… + + Lijsten + Weet je zeker dat je deze lijst definitief wilt verwijderen? + Er is nog niks in deze lijst. Wanneer lijstleden nieuwe toots publiceren, zijn deze hier te zien. + Aan lijst toevoegen + Lijst toevoegen + Lijst verwijderen + Lijst bewerken + Naam nieuwe lijst + Het account is aan de lijst toegevoegd! + Je hebt nog geen lijsten! + + %1$s is naar %2$s verhuisd + Problemen met aanmelden? + Hier zijn enkele suggesties die wellicht helpen:\n\n + - Controleer of er geen spelfout in de domeinnaam van de server zit\n\n + - Controleer of er de server niet offline is\n\n + - Wanneer je tweestapsverificatie gebruikt, gebruik dan (nadat je de domeinnaam van de server hebt ingevuld) de link daaronder.\n\n + - Je kan deze link ook gebruiken zonder dat je van tweestapsverificatie gebruikt maakt (wanneer het op dit aanmeldscherm niet lukt).\n\n + - Wanneer het nu nog steeds niet lukt, maak dan een nieuwe issue aan op Framagit via https://framagit.org/tom79/fedilab/issues + + Media is geladen. Klik hier om het te tonen. + Deze actie kan vrij lang duren. Je krijgt een melding wanneer het klaar is. + Nog steeds bezig, nog even wachten… + Toots exporteren + Toot van %1$s exporteren + %1$s van de %2$s toots zijn geëxporteerd. + Er is iets mis gegaan tijdens het exporteren van de gegevens van %1$s + Er is iets misgegaan tijdens het exporteren van de gegevens! + Er is iets misgegaan tijdens het importeren van de gegevens! + + Proxy + Proxy inschakelen? + Host + Poort + Gebruikersnaam + Wachtwoord + Volledige toot delen + Ondersteun de app op Liberapay + Er zit een fout in de reguliere expressie! + Er zijn geen tijdlijnen op deze server gevonden! + Server verwijderen? + Vertalen in + Server volgen + Je volgt deze server al! + De server wordt gevolgd! + Partnerschappen + Informatie + Boosts verbergen van %s + Uitlichten op profiel + Toon boosts van %s + Niet uitlichten op profiel + Het account is nu uitgelicht op het profiel + Het account is niet langer uitgelicht op het profiel + Boosts worden nu getoond! + Boosts worden nu verborgen! + Direct bericht + Filters + Geen filters om weer te geven. Je kunt er een maken door op de \"+\" knop te drukken. + Sleutelwoord of zin + Home tijdlijn + Publieke tijdlijn + Notificaties + Conversaties + Zal gematched worden ongeacht van hoofdlettergevoeligheid in tekst of content waarschuwing van een toot + Droppen in plaats van verbergen + Gefilterde toots verdwijnen onomkeerbaar, zelfs als een filter later is verwijderd + Wanneer het sleutelwoord of zin enkel alfanumeriek is, zal het enkel worden gematched op het hele woord + Heel woord + Filter contexten + Een of meerdere contexten waar de filter van toepassing zou zijn + Vervallen na + Filter verwijderen? + Filter bijwerken + Filter aanmaken + Wie te volgen + Er zijn momenteel geen accounts op deze lijst! + Volgen + Alles selecteren + Alles deselecteren + %s wordt gevolgd! + De lijst %s aanmaken + Accounts toevoegen aan de lijst + Accounts zijn toegevoegd aan de lijst + Accounts aan de lijst toevoegen + Je hebt nog geen lijst gemaakt. Klik op de \"+\" knop om een nieuwe toe te voegen. + Wie te volgen + Trunk-API + Account(s) kunnen niet worden gevolgd + Extern account ophalen + Automatisch als gevoelig gemarkeerde media uitklappen + Nieuwe volger + Nieuwe boost + Nieuwe favoriet + Nieuwe vermelding + Poll is beëindigd + Nieuwe toot + Toots back-uppen + New posts + Media downloaden + Meldingsgeluid veranderen + Selecteer toon + Tijdslot inschakelen + How-to-video\'s + Extern gesprek ophalen! + Geen geblokkeerde domeinen! + Domein deblokkeren + Weet je zeker dat je %s wilt deblokkeren? + Weet je zeker dat je %s wilt blokkeren? + Geblokkeerde domeinen + Domein blokkeren + Het domein is geblokkeerd + Het domein wordt niet langer geblokkeerd! + Extern bericht ophalen + Reageren + PeerTube-server + Wees de eerste om op deze video te reageren m.b.v. de knop rechtsboven! + %s keer bekeken + Duur: %s + Een server toevoegen + Reageren op deze video is uitgeschakeld! + Kies een resolutie + PeerTube-favorieten + De video is aan de bladwijzers toegevoegd! + De video is uit de bladwijzers verwijderd! + Er zitten geen PeerTube-video\'s tussen jouw favorieten! + Kanaal + Video\'s + Kanalen + Emoji One gebruiken + Informatie + In alle toots voorvertoningen tonen + Nieuwe ontwerper UX/UI + Voorbeeldvideo\'s tonen + Het account-ID is naar het klembord gekopieerd! + Taal veranderen + Standaardtaal + Lange toots afbreken + Toots langer dan \'x\' regels afbreken. Nul is niet afbreken. + Meer weergeven + Minder weergeven + Tags beheren + Deze tag bestaat al! + De tag is opgeslagen! + De tag is veranderd! + De tag is verwijderd! + Boost inplannen + De boost is ingepland! + Geen ingeplande boost om weer te geven! + Boost inplannen.]]> + Kunsttijdlijn + Menu openen + Ga terug + Logo van de applicatie + Avatar + Omslagfoto op profiel + Contact opnemen met de serverbeheerder + Nieuwe toevoegen + Logo MastoHost + Emojikiezer + Verversen + Gesprek uitbreiden + Account verwijderen + Het geblokkeerde domein verwijderen + Aangepaste emojikiezer + Video bekijken + Nieuwe toot + Afbeelding van de kaart + Media verbergen + Favicon + Media voor het toevoegen van een beschrijving + + Nooit + 30 minuten + 1 uur + 6 uur + 12 uur + 1 dag + 1 week + + In dit veld moet je de hostnaam van jouw server invullen.\nVoorbeeld: als je jouw account hebt aangemaakt op https://mastodon.social\n vul je mastodon.social in (dus zonder https://)\n +Je kunt beginnen met typen en er zullen namen gesuggereerd worden.\n\n +⚠ De inlogknop werkt alleen als de naam van de server geldig is, en de server online is! + Meer informatie + + Talen + Alleen media + NSFW tonen + Crowdinvertalingen + Crowdinbeheerder + Vertaling van de applicatie + Over Crowdin + Bot + Pixelfedserver + Server + Eén van deze + Al deze + Geen van deze + Eén van deze woorden (door spaties gescheiden) + Al deze woorden (door spaties gescheiden) + Voeg een paar woorden aan het filter toe (gescheiden met spaties) + Kolomnaam wijzigen + Misskeyserver + Er is geen app op je apparaat geïnstalleerd om deze link mee te openen. + Abonnementen + Overzicht + Trending + Onlangs toegevoegd + Lokaal + Uploaden + Reageren + Opmerking verwijderen + Weet je zeker dat je deze opmerking wilt verwijderen? + Volledig scherm + Manier om video\'s te bekijken + Kies een bestand om te uploaden + Mijn video\'s + Titel + Licentie + Categorie + Taal + Deze video bevat inhoud voor volwassen of gevoelige inhoud + Reacties op de video toestaan + Video bijwerken + Omschrijving + De video is bijgewerkt! + Uploaden geannuleerd! + De video is geüpload! + Aan het uploaden, een ogenblik geduld… + Klik hier om de videogegevens te bewerken. + Video verwijderen + Weet je zeker dat je deze video wilt verwijderen? + NSFW video\'s weergeven + Geen video\'s om weer te geven! + Reageren + Delen + Kies een manier van inplannen + Vanaf apparaat + Vanaf server + Toots (server) + Toots (apparaat) + Aanpassen + Nieuwe toots boven de \'Meer ophalen\' knop weergeven + Tijdlijnen + Gebruikersomgeving + Contacten + %1$s heeft op je video gereageerd%2$s]]> + %1$s volgt nu je kanaal %2$s]]> + %1$s volgt nu je account]]> + %1$s is gepubliceerd]]> + %1$s is geslaagd]]> + %1$s is mislukt]]> + %1$s heeft een nieuwe video gepubliceerd: %2$s]]> + %1$s is op de zwarte lijst gezet]]> + %1$s is van de zwarte lijst verwijderd]]> + Gegevens exporteren + Gegevens importeren + Kies het bestand om te importeren + Er is een fout opgetreden tijdens het kiezen van het back-upbestand! + Een openbare reactie toevoegen + Reactie versturen + Er is geen internetverbinding. Jouw bericht is als concept opgeslagen. + Platte tekst + HTML + Markdown + Account uitloggen + Alle + Ondersteun deze app + Met Open Collective kunnen groepen snel een collectief opzetten, geld inzamelen en dit transparant beheren. + Link kopiëren + Verbinden + Normaal + Compact + Console + Weergavemodus + Veiligheid verbeteren (Conscrypt) + Trackingdomeinen bijwerken + De trackingdatabase is bijgewerkt! + door de applicatie geblokkeerde http-aanvragen + Lijst met geblokkeerde aanvragen + Verzenden + De database is geëxporteerd! + Uitgelichte hashtags + Tijdlijn op tags filteren + Geen tags + De \"verwijderen\"-knop in de notificatiekolom verbergen + Een afbeelding toevoegen wanneer een URL wordt gedeeld + + Poll + Polls + Een poll maken + Keuze 1 + Keuze 2 + Keuze %d + Je hebt tenminste twee keuzes nodig voor een poll! + Klaar + eindigt op %s + Poll vernieuwen + Stemmen + Een poll waarin je hebt gestemd is beëindigd + Een poll die je hebt getoot is beëindigd + Aanpassen + Categorieën + Tijdslot + Geavanceerd + Badge met \'nieuw\' op ongelezen toots tonen + PeerTube + Tijdlijn verplaatsen + Tijdlijn verbergen + Tijdlijnen herordenen + Lijst is definitief verwijderd + Gevolgde server verwijderd + Vastgezette tag verwijderd + Ongedaan maken + Je moet twee zichtbare tabbladen behouden! + Tijdlijnen herordenen + Hoofdtijdlijnen kunnen alleen worden verborgen! + BBCode + Media altijd als gevoelig markeren + GNU social-server + Gebufferd bericht + Tags hergebruiken in reacties + Lang indrukken om media op te slaan + Gevoelige media vervagen + Tijdlijnen in een lijst tonen + Tijdlijnen tonen + Botaccounts in toots markeren + Tags beheren + Positie in starttijdlijn onthouden + Geschiedenis + Afspeellijsten + Weergavenaam + Je hebt geen enkele afspeellijst. Klik op het \"+\"-pictogram om een nieuwe afspeellijst toe te voegen + Je moet een weergavenaam invullen! + Het kanaal is verplicht wanneer de afspeellijst openbaar is. + Afspeellijst aanmaken + De afspeellijst is nog leeg. + opnieuw + Gallerij + Emoji + Sticker + Gum + Tekst + Filter + Kwast + Weet je zeker dat je wilt afsluiten zonder de afbeelding op te slaan? + Weggooien + Opslaan… + Afbeelding succesvol opgeslagen! + Opslaan van afbeelding mislukt + Ondoorzichtigheid + Fotobewerker inschakelen + Een pollkeuze toevoegen + Laatste pollkeuze verwijderen + Gesprek negeren + Ondeugend gesprek + Het gesprek is niet meer gedempt! + Het gesprek is gedempt + Functies voor open toepassingen + Getimede demper + Vermeld de rekening + Verfrissende cache + Vermeld de status + Nieuws + Algemeen + Regionale + Kunst + Journalistiek + Activisme + Gaming + Technologie + Verborgen inhoud + Bont + Voedsel + Logo van het geval + Er is iets misgegaan bij het controleren van de beschikbare instanties! + Ga mee met Mastodon + Kies een instantie door een categorie op te pakken, klik dan op een controleknop. + Kies een instantie door op een controleknop te tikken. + %1$s users + Bevestig wachtwoord + I agree to %1$s and %2$s + serverregels + terms of service + Sign up + Dit geval werkt met uitnodigingen. Uw account moet handmatig worden goedgekeurd door een beheerder voordat het bruikbaar is. + Please, fill all the fields! + Wachtwoorden komen niet overeen! + De e-mail lijkt niet geldig te zijn! + Your username will be unique on %1$s + U ontvangt een bevestigingsmail + Gebruik minstens 8 karakters + Password should contain at least 8 characters + Username should only contain letters, numbers and underscores + Account aangemaakt! + Your account has been created!\n\n + Think to validate your email within the 48 next hours.\n\n + You can now connect your account by writing %1$s in the first field and click on Connect.\n\n + Important: If your instance required validation, you will receive an email once it is validated! + + Het bericht opslaan in concepten? + Administratie + Rapporten + Geen meldingen te tonen! + Verbind het account opnieuw + The application failed to access the admin features. You might need to reconnect the account for having the correct scope. + Onopgelost + Remote + Actief + Pending + Uitgeschakeld + Gedempt + Suspended + Toestemmingen + E-mailstatus + Aanmeldingsstatus + Joined + Meest recente IP + Waarschuwing + Disable + Silence + Notify the user per e-mail + Custom warning + Gebruiker + Presentator + Administrator + Confirmed + Not confirmed + Reported statuses + Account + Undo silence + Undo disable + Suspend + Undo suspend + The account is silenced! + The account is no longer silenced! + The account is suspended! + The account is no longer suspended! + The account is disabled! + The account is no longer disabled! + The account has been warned! + Display the admin menu + Display the admin feature in statuses + Allow + The account is approved! + The account is rejected! + Assign to me + Unassign + Mark as resolved + Mark as unresolved + Empty content! + Display Fedilab features button + The application needs to access audio recording + Voice message + Enable quick reply + The account you are replying might not see your message! + If disabled, the app will always load last statuses + If disabled, sensitive media will be hidden with a button + Store media in full size with a long press on previews + Add an ellipse button at the top right for listing all tags/instances/lists + During the time slot, the app will send notifications. You can reverse (ie: silent) this time slot with the right spinner. + Display a Fedilab button below profile picture. It is a shortcut for accessing in-app features. + Allow to reply directly in timelines below statuses + Previews will not be cropped in timelines + Allow to play embedded videos directly in timelines + Allow to reverse the way to read statuses that are displayed once tapping the fetch more button + This option allows to support recent cipher suites. It is useful for older Android devices or if you cannot connect to your instance. + Exclusively for Peertube videos. Switch this mode if you cannot play them. + These tags will allow to filter statuses from profiles. You will have to use the context menu for seeing them. + Automatically insert a line break after the mention to capitalize the first letter + Allow content creators to share statuses to their RSS feeds + Compose + Maximum retry times when uploading media + Create a new Folder here + Enter the folder name + Please enter a valid folder name + This folder already exists.\n Please provide another name for the folder + Select + Default Directory + Folder + Create folder + Display a toast message after an action has been completed (boost, fav, etc.)? + Muted instances have been exported! + Add an instance + Export instances + Import instances + Crash reports + Enable crash reports + If enabled, a crash report will be created locally and then you will be able to share it. + Fedilab is gestopt :( + Je kunt mij per e-mail foutrapportages sturen. Dit helpt om het probleem op te lossen :)\n\nJe kunt extra informatie toevoegen. Dank je! + Use the wysiwyg + When enabled, you will be able to format your text easily with tools. + Statistics + Total statuses + Number of boosts + Number of favourites + Number of mentions + Number of follows + Number of polls + Number of replies + Number of statuses + Statuses + Visibility + Number with media + Number with sensitive media + Number with CW + First status date + Last status date + First notification date + Last notification date + Frequency + %s statuses per day + %s notifications per day + Date range + Groups + No groups! + Disable custom animated emojis + Charts + Display charts + The application collects your local data, please wait… + Backup + Auto backup statuses + This option is per account. It will launch a service that will automatically store your statuses locally in the database. That allows to get statistics and charts + Auto backup notifications + This option is per account. It will launch a service that will automatically store your notifications locally in the database. That allows to get statistics and charts + Report account + Send an invitation + Your instance does not allow to register a new account! + + %d stem + %d stemmen + + + %d voter + %d voters + + + Enkele keus + Meerdere keuzes + + + 5 minuten + 30 minuten + 1 uur + 6 uur + 1 dag + 3 dagen + 7 dagen + + + Via WebTorrent + Rechtstreeks via http + + For joining my instance \"%1$s\", you can download Fedilab:\n\nF-Droid: %2$s\nGoogle: %3$s\n\nThen open the link below with Fedilab and create your account :)\n\n%4$s + + Your poll can\'t have duplicated options! + For all accounts + Database cache + Clear your home timeline cache + Clear your cached statuses + Clear your bookmarks + Files in cache + Total notifications + Hide menu items + Fedilab is running live notifications + For %1$s accounts with %2$s events + Live notifications for %1$s + Live notifications will be enabled for this account. + Clear cache when leaving + The cache (media, cached messages, data from the built-in browser) will be automatically cleared when leaving the application. + Do you want to unfollow this account? + Show confirmation dialog before unfollowing + Replace Youtube with Invidio.us + Invidious is an alternative front-end to YouTube + Enter your custom host or leave blank for using invidious.snopyta.org + Replace Twitter with Nitter + Nitter is an open source alternative Twitter front-end focused on privacy. + Enter your custom host or leave blank for using nitter.net + Replace Instagram with Bibliogram + Bibliogram is an open source alternative Instagram front-end focused on privacy. + Enter your custom host or leave blank for using bibliogram.art + Replace Reddit with Libreddit + Libreddit is an open source alternative Reddit front-end focused on privacy. + Enter your custom host or leave blank for using libredd.it + Replace Medium links + Replace medium.com links with an open source alternative front-end focused on privacy. + Default: scribe.rip + Replace Wikipedia links + Replace Wikipedia link with an open source alternative front-end focused on privacy. + Default: wikiless.org + Hide Fedilab notification bar + For hiding the remaining notification in the status bar, tap on the eye icon button then uncheck: \"Display in status bar\" + Use a push notifications system for getting notifications in real time. + No live notifications + Live notifications + Notifications will be fetched every 15 minutes. + Add notes + Notes for the account + Allow to compress large photos into smaller sized photos with very less or negligible loss in quality of the image. + Allow compressing videos while maintaining their quality. + The app is compressing the media, it can be quite long… + Change app icon + Tap to change the app icon + Post + Visibility of the post + Tap here to add photos + Accepted Formats: jpeg, png, gif \n\nMax File Size: 15 MB \n\nAlbums can contain up to 4 photos or videos + Upload media + Add an optional caption + The app received a very long error message from the API %1$s + Message preview + Add mentions in each message + Fetching conversation + Order by + Title for the video + Join Peertube + I am at least 16 years old and agree to the %1$s of this instance + Links + Change the color of links (URLs, mentions, tags, etc.) in messages + Reblogs header + Change the color of display name at the top of messages + Change the color of the user name at the top of messages + Change the color of the header for reblogs + Posts + Background color of posts in timelines + Reset colors + Tap here to reset all your custom colors + Reset + Icons + Color of bottom icons in timelines + Pin this tag + Logo of the instance + Profiel bewerken + Maak een actie + Vertaling + Beeldvoorbeeld + Tekstkleur + Verander de tekstkleur in potten + Veranderingen toepassen + U moet de toepassing opnieuw opstarten om de wijzigingen toe te passen + Herstart + Gebruik een aangepast thema + Laat de kleuren van het geselecteerde thema hierboven overrulen + Theming + Opslaan voor + Het thema werd geëxporteerd + Het thema is met succes geëxporteerd in CSV + Breng de primaire kleur aan op de statusbalk + Kleur van de statusbalk + Een standaard thema herstellen + Een thema importeren + Tik hier om een thema uit een eerdere export te importeren + Exporteer het thema + Tik hier om het huidige thema te exporteren + Er is een fout opgetreden bij het selecteren van het themabestand + Thema Picker + Selecteer een vooraf geïnstalleerd thema + Thema\'s + Breng de primaire kleur aan op de navigatiebalk + Kleur van de navigatiebalk + De onderliggende kleur van de inhoud van de app. + Achtergrondkleur + Accenten selecteren delen van de UI. + Accent kleur + Wordt het vaakst weergegeven in uw app. + Primaire kleur + Exporteer bladwijzers naar de instantie + Importeer bladwijzers van de instantie + Aantal gebruikers + Status telling + Instance count + Geblokkeerd + Eindigt over %s + Wat is er nieuw in %s + Je kunt mijn account voor updates volgen + Dit exemplaar is niet beschikbaar op https://instances.social + Add a comment + Deelverbinding + De URL is gekopieerd naar het klembord + Open met een andere App + Omleiding controleren + Deze URL stuurt niet door + %1$s \n\nredirects to\n\n %2$s + Wijzig de gebruikersagent + Stel een aangepaste gebruikersagent in of laat leeg + Maakt het mogelijk om de gebruikersagent die wordt gebruikt voor api-gesprekken of met de ingebouwde browser aan te passen. + UTM-parameters verwijderen + The app will automatically remove UTM parameters from URLs before visiting a link. + Trends + Nu Trending + %d mensen die praten + Twitteraccounts (via Nitter) + Twitter gebruikersnamen ruimte gescheiden + Identiteitsbewijzen + Geverifieerde identiteit + Verified by %1$s (%2$s) + De kennisgeving verwijderen + Meer opties weergeven + Het is een Pixelfed-verhaal + Upload een medium, het wordt automatisch toegevoegd aan je Pixelfed-verhaal. + Media succesvol toegevoegd aan uw verhaal! + Actie gehandicapt + Ontvouw + Er is iets misgegaan, controleer uw download directory in de instellingen. + Aankondigingen + Geen mededelingen! + Reactie toevoegen + Gebruik uw favoriete browser in de app. Schakel deze functie uit om externe links te openen. + Videocache in MB, nul betekent geen cache. + Watermerken + Voeg automatisch een watermerk toe aan de onderkant van de foto\'s. De tekst kan voor elk account worden aangepast. + No distributors found! + You need a distributor for receiving push notifications.\nYou will find more details at %1$s.\n\nYou can also disable push notifications in settings for ignoring that message. + Select a distributor + diff --git a/app/src/main/res/values-no/strings.xml b/app/src/main/res/values-no/strings.xml new file mode 100644 index 000000000..240acfa6f --- /dev/null +++ b/app/src/main/res/values-no/strings.xml @@ -0,0 +1,1132 @@ + + + Åpne menyen + Lukk menyen + Om + Om instansen + Personvern + Mellomlager + Logg ut + Logg Inn + + Lukk + Ja + Nei + Avbryt + Last ned + Last ned %1$s + Media lagret + Fil: %1$s + Passord + E-post + Kontoer + Toots + Stikkord + Lagre + Gjenopprett + Ingen resultater! + Instans + Instans: mastodon.social + Bruker nå kontoen %1$s + Legg til en konto + Tootet er kopiert til utklippstavlen! + Nettadressen til tootet er kopiert til utklippstavlen + Endre + Velg et bilde… + Fjern + Kamera + Vil du slette alt? + Oversett dette tootet. + Planlegg + Tekst- og ikonstørrelser + Endre gjeldende tekststørrelse: + Endre gjeldende ikonstørrelse: + Neste + Forrige + Åpne med + OK + Media + Del med + Delt via Fedilab + Svar + Brukernavn + Utkast + Favoritter + Nye følgere + Nevnelser + Boosts + Vis boosts + Vis svar + Åpne i nettleseren + Oversett + Vennligst vent noen sekunder før du utfører denne handlingen. + + Hjem + Lokal tidslinje + Forent tidslinje + Alternativer + Favoritter + Kommunikasjon + Dempede brukere + Blokkerte brukere + Varsler + Forespørsler om følging + Innstillinger + Slett en konto + Slett kontoen %1$s fra applikasjonen? + Send en epost + Klikk på stien for å endre den + Feilet! + Planlagte toots + Informasjon nedenfor kan gjenspeile brukerens profil ufullstendig. + Sett inn emoji + Appen samler ikke tilpassede emojis for øyeblikket. + Push notifications + Are you sure you want to logout? + Are you sure you want to logout @%1$s@%2$s? + + Ingen toots å vise + Ingen historier å vise + Historier + Boostet av %1$s + Legge dette tootet til dine favoritter? + Fjerne dette tootet fra favorittene dine? + Booste dette tootet? + Fjerne boosten på dette tootet? + Feste dette tootet? + Løsne dette tootet? + Demp + Blokker + Rapporter + Fjern + Kopier + Del + Nevn + Demp tidsbestemt + Slett og kladd på nytt + + Dempe denne kontoen? + Blokkere denne kontoen? + Rapportere dette tootet? + Blokkere dette domenet? + Fjern demping for denne kontoen? + Unblock this account? + + + Varsle + Stille + + + Fjerne dette tootet? + Slette og kladde dette tootet på nytt? + + Bokmerker + Legg til bokmerke + Fjern fra bokmerker + Ingen bokmerker å vise + Tootet er lagt til bokmerker! + Tootet er fjernet fra bokmerker! + + %d s + %d m + %d h + %d d + + %d sekund + %d sekunder + + + %d minutt + %d minutter + + + %d time + %d timer + + + %d dag + %d dager + + + Innholdsadvarsel + Hva tenker du på? + TOOT! + QUEET! + cw + Skriv et toot + Svar på et toot + Skriv et queet + Svar på et queet + Velg et medie + Det oppsto en feil under valg av media! + Slett dette mediet? + Din toot er tom! + Synlighet av tootet + Standardsynlighet på toots: + Tootet er sendt! + Du svarer på dette tootet: + Sensitivt innhold? + + Post til offentlige tidslinjer + Ikke post til offentlige tidslinjer + Bare post til følgere + Bare post til nevnte brukere + + Ingen lagrede utkast! + Velg et toot + Velg en konto + Velg en eller flere kontoer + Slette utkast? + Klikk på knappen for å vise originaltootet + Beskriv for synshemmede + + Ingen beskrivelse tilgjengelig! + + Versjon %1$s + Utvikler: + Lisens: + GNU GPL V3 + Kildekode: + Oversetting av toots: + Søk instanser: + Ikon-designer: + + Samtale + + Ingen konto å vise + Ingen følg-forespørsel + Toots \n %1$s + Følger \n %1$s + Følgere \n %1$s + Pinned \n %d + Autorisere + Avslå + + Ingen planlagte toots å vise! + Skriv et toot og velg deretter Planlegg fra toppmenyen. + Slette planlagt toot? + Medier: %d + Tootet har blitt planlagt! + Den planlagte datoen må være senere enn den nåværende timen! + Batterisparer er aktivert! Det virker kanskje ikke som forventet. + + Tiden for demping bør være mer enn ett minutt. + %1$s er dempet til %2$s. \n Du kan også slå av demping manuelt igjen fra han/hennes profil. + %1$s er dempet til %2$s. \n Klikk her for å slå kontoen igjen. + + Ingen varslinger for å vise + nevnte deg + wrote a new message + boostet tootet ditt + favoriserte tootet ditt + fulgte deg + bedt om å følge deg + + og et annet varsel + og %d andre varsler + + + %d liker + %d liker + + Slett et varsel? + Slett alle varsler? + Varselet er slettet! + Alle varsler er slettet! + + Følger + Følgere + Festet + + Kunne ikke hente klient-id! + Unable to connect to instance domain! + Ingen Internett-tilkobling! + Kontoen ble blokkert! + Kontoen er ikke lenger blokkert! + Kontoen ble dempet! + Kontoen er ikke lenger dempet! + Kontoen ble fulgt! + Kontoen er ikke lenger fulgt! + Tootet ble boostet! + Tootet er ikke lenger boostet! + Tootet ble lagt til dine favoritter! + Tootet ble fjernet fra dine favoritter! + Tootet ble rapportert! + Tootet ble slettet! + Tootet ble festet! + Tootet ble løsnet! + Ups! En feil oppstod! + En feil oppstod! Instansen returnerte ikke en autorisasjonskode! + Adressen til instansen ser ikke ut til å være gyldig! + Det oppsto en feil under bytte mellom kontoer! + Det oppsto en feil under søking! + Profildataene er lagret! + Ingen handling kan tas + Media har blitt lagret! + Det oppstod en feil under oversettelsen! + Oversettelse er skrudd av i innstillingene + Utkast lagret! + Er du sikker på at denne instansen tillater dette antall tegn? Vanligvis er denne verdien nær 500 tegn. + Synligheten til toots er endret for kontoen %1$s + + Antall toots per innlasting + Alltid + WIFI + Spør + Last media + Last bildene + Vis mer… + Vis mindre… + Sensitivt innhold + Deaktiver GIF-avatarer + Sti: + Lagre utkast automatisk + Legg til URL til media i toots + Varlse når noen følger deg + Varsle når noen booster tootene dine + Varsle når noen favoriserer tootene dine + Varsle når noen nevner deg + Varsle når en avstemming er avsluttet + Notify for new posts + Vis bekreftelsesdialog før du booster + Vis bekreftelsesdialog før du legger til i favoritter + Varsle kun på WIFI + Varsle? + Dempede varslinger + NSFW visningspause i sekunder (0 betyr ingen visningspause) + Media Description timeout (seconds, 0 means off) + Rediger profil + Tilpasset deling + Din URL for tilpasset deling… + Bio… + Lås konto + Lagre endringer + Velg et toppbilde + Tilpass forhåndsvisningsbilder + Del opp toots over 500 tegn i svar + Du har nådd de 160 tegnene som er tillatt! + Du har nådd de 30 tegnene som er tillatt! + Mellom + og + Tiden må være større enn %1$s + Tiden må være mindre enn %1$s + Starttid + Sluttid + Bruk den innebygde nettleseren + Egendefinerte faner + Aktiver Javascript + Utvid cw automatisk + Tillat cookies fra tredjeparter + API-nøkkel, du kan la denne stå tom dersom Yandex benyttes + + Mørkt + Lyst + Svart + + Angi LED-farge: + + Blå + Cyan + Magenta + Grønn + Rød + Gul + Hvit + + Følg + Slutt å blokkere + Demp + Skru av demping + Forespørsel sendt + Følger deg + Søk + Stor forbokstav i svar + Endre størrelsen på bilder + Endre størrelse på videoer + + Push-varsler + Vennligst bekreft push notifications som du vil motta. +Du kan aktivere eller deaktivere disse meldingene senere i innstillingene (Meldinger-fanen). + + Slett mellomlager + Det er %1$s med data i mellomlageret. \n \nVil du slette alle data fra mellomlageret? + Mb + Cachen ble ryddet! %1$s ble utgitt + + Tittel + Tittel… + Beskrivelse + Nøkkelord + Nøkkelord… + + Synkroniser + Filter + Dine toots + Varslene dine + Offentlig + Ikke listet + Privat + Direkte + Søkeord… + Vis media + Vis festede + Ingen treff! + Sikkerhetskopier toots for %1$s + %1$s nye toots er importert + %1$s nye varsler er importert + + Dates descending + Dates ascending + + + Nei + Bare + Begge + + Ingen toots ble funnet. Bruk Oppdater-knappen oppe til høyre for å hente dem. + + Lagrede data + Bare grunnleggende informasjon fra kontoer lagres på enheten. + Disse dataene er strengt konfidensielle og kan bare brukes av applikasjonen. + Hvis du sletter programmet, fjernes disse dataene. \n + ⚠ Brukernavn og passord lagres aldri. De brukes kun under sikker autentisering (SSL) med en instans. + Tillatelser: + ACCESS_NETWORK_STATE : Brukes til å oppdage om enheten er koblet til et WIFI-nettverk. \n +         - INTERNET : Brukes for spørringer til en instans. \n +         - WRITE_EXTERNAL_STORAGE : Brukes til å lagre media eller flytte appen på et SD-kort. \n +         - READ_EXTERNAL_STORAGE : Brukes til å legge til media til toots. \n +         - BOOT_COMPLETED : Brukes til å starte varslingstjenesten. \n +         - WAKE_LOCK : Brukes under varsling. + API-tillatelser: + - Les: Les data. \n +- Skriv: Legg inn statuser og last opp medier for statuser. \n +- Følg: Følg, unollow, block, unblock. \n \n +⚠ Disse handlingene utføres bare når brukeren ber om dem. + Sporing og biblioteker + Applikasjonen bruker ikke sporingsverktøy (målgruppemåling, feilrapportering, etc.), og inneholder ingen reklame. \n \n +         Bruken av biblioteker er minmal: \n +         - Glide: For å administrere media \n +         - Android-job: For å administrere tjenester \n +         - PhotoView: For å administrere bilder \n + Oversettelse av toots + Programmet tilbyr muligheten til å oversette toots ved hjelp av lokasjonen til enheten og Yandex APIet. \n +         Yandex har sin egen personvernerklæring som finnes her: https://yandex.ru/legal/confidential/?lang=en + Takk til: + Filtrer ut med regulære uttrykk + Søk + Slett + Hent flere toots… + + Lister + Er du sikker på at du vil slette dette filteret? + Det er ingenting i denne listen. Når medlemmer av denne listen legger inn nye statuser, vises de her. + Legg til i liste + Opprett ny liste + Slett liste + Rediger liste + Tittel på liste + Kontoen ble lagt til på listen! + Du har ikke noen lister ennå! + + %1$s er flyttet til %2$s + Får du ikke logget inn? + Her er noen ting du kan sjekke:\n\n +- Kontroller at det ikke er noen skrivefeil i adressen til instansen\n\n +- Kontroller at instansen ikke er nede (offline)\n\n +- Hvis du bruker tofaktorautentisering (2FA), vennligst benytt linken på bunnen (etter at adressen til instansen er fylt inn)\n\n +- Du kan også benytte denne linken for å logge inn med 2FA\n\n +- Hvis det fortsatt ikke fungerer å koble til instansen, vennligst opprett en sak på Framagit, https://framagit.org/tom79/fedilab/issues\n\n + Mediet er lastet. Klikk her for å vise. + Denne handlingen kan ta lang tid. Du vil bli varslet når det er ferdig. + Kjører fortsatt, vennligst vent… + Eksporter statuser + Eksportere statuser for %1$s + %1$s av %2$s toots har blitt eksportert. + Noe gikk galt under eksport av data for %1$s + Noe gikk galt under eksport av data! + Noe gikk galt under import av data! + + Proxy + Aktivere proxy? + Adresse + Port + Brukernavn + Passord + Legg til toot-detaljer ved deling + Støtt denne applikasjonen med Liberapay + Det er en feil i det regulære uttrykket! + Fant ingen tidslinjer på denne instansen! + Slette denne instansen? + Oversett med + Følg instanse + Du følger allerede denne instansen! + Instansen følges! + Samarbeid + Informasjon + Skjul boosts fra %s + Vis på profilen + Vis boosts fra %s + Ikke vis på profilen + Brukerkontoen blir nå vist på profilen + Brukerkontoen er ikke lenger vist på profilen + Boosts blir nå vist! + Boosts er nå skjult! + Direktemelding + Filtre + Du har ikke opprettet noen filtre. Du kan opprette et filter ved å klikke på \"+\"-knappen. + Nøkkelord eller et uttrykk + Lokal tidslinje + Offentlig tidslinjer + Varsler + Samtaler + Vil bli matchet uavhengig av store og små bokstaver i tekst eller Innholdsadvarsel på et toot + Forkast i stedet for å skjule + Filtrerte toots vil ikke bli vist selv om filteret fjernes senere + Når søkeordet eller setningen bare er alfanumerisk, aktiveres filteret bare hvis det samsvarer med hele ordet + Hele ord + Filterkontekst + En eller flere kontekster hvor filteret skal gjelde + Utløper etter + Slette filteret? + Oppdater filter + Opprett filter + Hvem å følge + Det er ingen kontoer oppført for øyeblikket! + Følg + Velg alle + Fjern alle valg + %s følges! + Oppretter listen %s + Legg til brukerkontoer i listen + Brukerkontoer ble lagt til listen + Legger brukerkontoer til listen + Du har ikke opprettet noen lister. Klikk på \"+\"-knappen for å opprette en ny. + Hvem å følge + Trunk API + Brukerkonto(ene) kan ikke følges + Henter ekstern brukerkonto + Utvid skjult media automatisk + Ny følger + Ny boost + Ny favoritt + Ny nevning + Avstemming avsluttet + Ny toot + Backup av toots + New posts + Last ned media + Endre varsellyd + Velg varsellyd + Aktiver tidsvindu + Instruksjonsvideoer + Henter eksterne tråd! + Ingen blokkerte domener! + Oppheve blokkering av domenet? + Er du sikker på at du vil å oppheve blokkering av %s? + Er du sikker på at du vil blokkere %s? + Blokkerte domener + Blokker domenet + Domenet er blokkert + Domenet er ikke lenger blokkert! + Henter ekstern status + Kommentar + Peertube-instans + Vær den første til å kommentere på denne videoen. Bruk knappen oppe til høyre! + %s visninger + Varighet: %s + Legg til instans + Kommentarer er ikke aktivert på for denne videoen! + Velg en oppløsningen + Peertube-favoritter + Videoen er lagt til bokmerker! + Videoen er fjernet fra bokmerker! + Det er ingen Peertube-videoer blant favorittene dine! + Kanal + Videoer + Kanaler + Bruk Emoji One + Informasjon + Forhåndsvis alle toots + Ny UX/UI-designer + Forhåndsvis videoer + Id på brukerkonto er kopiert til utklippstavlen! + Endre språk + Standardspråk + Avkort lange toots + Avkort toots lengre enn \'x\' linjer. 0 betyr at toots ikke avkortes. + Vis mer + Vis mindre + Administrer stikkord + Dette stikkordet eksisterer allerede! + Stikkordet er lagret! + Stikkordet er endret! + Stikkordet er slettet! + Planlegg boost + Boosten er planlagt! + Ingen planlagte boosts å vise! + Planlegg boost.]]> + Kunsttidslinje + Åpne meny + Tilbake + Applikasjonens logo + Profilbilde + Profilfane + Kontakt instansadministratoren + Legg til ny + MasoHost-logo + Emoji-velger + Oppdater + Utvid samtalen + Fjern en konto + Slett det blokkerte domenet + Egendefinert emoji-velger + Spill av video + Ny toot + Bilde på kortet + Skjul media + Favicon + Media for å legge til en beskrivelse + + Aldri + 30 minutter + 1 time + 6 timer + 12 timer + 1 dag + 1 uke + + I dette feltet skriver du adressen til instansen. Hvis du opprettet en konto på https://mastodon.social\n, skriv mastodon.social (uten https://)\n +Adresser vil bli foreslått når du begynner å skrive.\n\n +⚠ Logg inn-knappen vil bare fungere hvis navnet på instansen er gyldig, og instansen er oppe (online)! + Mer informasjon + + Språk + Kun media + Vis NSFW + Crowdin-oversettelser + Crowdin-administrator + Hjelp til å oversette applikasjonen på Crowdin + Åpne Crowdin + Robot + Pixelfed-instans + Mastodon-instans + Hvilke som helst av disse stikkordene + Alle disse stikkordene + Ingen av disse stikkordene + Hvilke som helst av disse stikkordene (separert med mellomrom) + Alle disse ordene (separert med mellomrom) + Legg noen ord til filteret (skill dem med mellomrom) + Endre kolonnenavn + Misskey-instans + Ingen app som støtter denne koblingen er installert på enheten. + Abonnementer + Oversikt + Populære + Nylig lagt til + Lokale + Last opp + Svar + Slett en kommentar + Er du sikker på at du vil slette denne kommentaren? + Fullskjerm video + Videomodus + Velg filen som skal lastes opp + Mine videoer + Tittel + Lisens + Kategori + Språk + Denne videoen har innhold som kan være støtende + Aktiver videokommentarer + Oppdater video + Beskrivelse + Videoen er oppdatert! + Opplasting avbrutt! + Videoen har blitt lastet opp! + Laster opp, vennligst vent… + Klikk her for å redigere videoinformasjon. + Slett video + Er du sikker på at du vil slette denne videoen? + Vis NSFW-videoer + Ingen videoer å vise! + Legg igjen en kommentar + Del + Velg en tidsplan-modus + Fra enhet + Fra server + Toots (Server) + Toots (enhet) + Endre + Vise nye toots over \"Hent flere toots…\"-knappen + Tidslinjer + Brukergrensesnitt + Kontakter + %1$s kommenterte på din video %2$s]]> + %1$s følger din kanal %2$s]]> + %1$s følger deg]]> + %1$s er publisert]]> + %1$s er importert]]> + %1$s mislyktes]]> + %1$s publiserte en ny video: %2$s]]> + %1$s er blitt svartelistet]]> + %1$s er ikke lenger svartelistet]]> + Eksporter data + Importer data + Velg filen som skal importeres + Det oppsto en feil under valg av fil! + Ny kommentar + Send kommentar + Enheten er ikke koblet til internett. Meldingen er lagret som kladd. + Ren tekst + HTML + Markdown + Logg ut + Alle + Støtt applikasjonen + Open Collective gjør det enkelt å sette opp et kollektiv, hente inn penger, og håndtere dem transparent. + Kopier lenke + Koble til + Normal + Kompakt + Konsoll + Angi visningsmodus + Oppdater sikkerhetstilbyder + Oppdater sporingsdomener + Sporingsdata er oppdatert! + HTTP-kall blokkert av applikasjonen + Liste over blokkerte kall + Send + Databasen er eksportert! + Utvalgte stikkord + Filtrer tidslinjen med stikkord + Ingen stikkord + Skjul slett-knappen + Tilknytt et bilde når du deler en URL + + Avstemning + Avstemminger + Lag en avstemming + Valg 1 + Valg 2 + Valg %d + Du trenger minst to valg i avstemmingen! + Ferdig + steng %s + Oppdater avstemming + Stem + En avstemming du har stemt på er avsluttet + En avstemming du har tootet er avsluttet + Tilpass + Kategorier + Tidsluke + Avansert + Vis \"ny\"-merket på uleste toots + Peertube + Flytt tidslinje + Skjul tidslinje + Endre rekkefølge på tidslinjer + Listen er slettet + Fulgt instans er fjernet + Festet emneord er fjernet + Angre + Du må ha to synlige faner! + Endre rekkefølge på tidslinjer + Hovedtidslinjer kan bare skjules! + BBCode + Marker alltid media som sensitivt + GNU-instans + Mellomlagret status + Videresend emneord i svar + Langt trykk for å lagre media + Dim sensitiv media + Vis tidslinjer i en liste + Vis tidslinjer + Marker robot-kontoer i toots + Administrer stikkord + Husk posisjonen i tidslinken + Historikk + Spilleliste + Visningsnavn + Du har ingen spillelister. Trykk på \"+\"-ikonet for å opprette en spilleliste + Du må oppgi et visningsnavn + Kanal er påkrevd når spillelisten er offentlig. + Opprett en spilleliste + Det er ingenting i denne spilleliste enda. + Gjør om igjen + Galleri + Emoji + Klistremerke + Viskelær + Tekst + Filter + Pensel + Er du sikker på at du vil avslutte uten å lagre bildet? + Forkast + Lagrer… + Bildet er lagret! + Klarte ikke å lagre bildet + Opasitet + Aktiver bilderedigering + Legg til valg + Fjern siste valg + Demp samtalen + Fjern demping av samtalen + Denne samtalen er ikke lenger dempet! + Samtalen er dempet + Åpne applikasjonsfunksjoner + Tidsbestemt demping + Nevn en konto + Oppdater mellomlager + Nevn statusen + Nyheter + Generelt + Regionalt + Kunst + Journalistikk + Aktivisme + Spilling + Teknologi + Voksent innhold + Furry + Mat + Instansens logo + Noe gikk galt under henting av tilgjengelige instanser! + Opprett Mastodon-konto + Velg en instans fra en av kategoriene. + Choose an instance by tapping on a check button. + %1$s brukere + Bekreft passord + Jeg er enig i %1$s og %2$s + regler for instansen + vilkår for tjenesten + Registrer deg + Instansen tillater bare nye medlemmer som er invitert. Kontoen din må manuelt godkjennes av en administrator før den kan brukes. + Vennligst fyll ut alle feltene! + Passordene er ikke like! + E-postadressen ser ikke ut til å være gyldig! + Brukernavnet ditt vil være unikt på %1$s + Du vil få tilsendt en bekreftelse på e-post + Bruk minst 8 tegn + Passordet må inneholde minst 8 tegn + Brukernavn skal bare inneholde bokstaver, tall og understrek + Konto opprettet! + Din konto har blitt opprettet!\n\n + Vurder å bekreft e-postadressen din innen 48 timer.\n\n + Du kan nå koble kontoen din ved å skrive %1$s i det første feltet og klikk på Koble til.\n\n + Viktig: Hvis instansen krever invitasjon, vil du motta en e-post når kontoen er godkjent! + Lagre meldingen som kladd? + Administrasjon + Rapporter + Ingen rapporter å vise! + Koble til kontoen på nytt + The application failed to access the admin features. You might need to reconnect the account for having the correct scope. + Uløst + Ekstern + Aktiv + Avventer + Deaktivert + Silenced + Suspendert + Tillatelser + E-poststatus + Login status + Ble med + Siste IP + Advar + Deaktiver + Silence + Gi beskjed til brukeren per e-post + Tilpasset advarsel + Bruker + Moderator + Administrator + Bekreftet + Ikke bekreftet + Rapporterte statuser + Konto + Undo silence + Undo disable + Suspender + Angre suspendere + The account is silenced! + The account is no longer silenced! + Kontoen er sperret! + The account is no longer suspended! + The account is disabled! + The account is no longer disabled! + Kontoen har blitt advart! + Display the admin menu + Display the admin feature in statuses + Tillat + Kontoen er godkjent! + Kontoen er forkastet! + Assign to me + Unassign + Merk som løst + Merk som uoppklart + Empty content! + Display Fedilab features button + Applikasjonen trenger tilgang til lydopptak + Talemelding + Enable quick reply + Det kan hende at kontoen du svarer ikke ser meldingen din! + If disabled, the app will always load last statuses + If disabled, sensitive media will be hidden with a button + Store media in full size with a long press on previews + Add an ellipse button at the top right for listing all tags/instances/lists + During the time slot, the app will send notifications. You can reverse (ie: silent) this time slot with the right spinner. + Display a Fedilab button below profile picture. It is a shortcut for accessing in-app features. + Allow to reply directly in timelines below statuses + Previews will not be cropped in timelines + Allow to play embedded videos directly in timelines + Allow to reverse the way to read statuses that are displayed once tapping the fetch more button + This option allows to support recent cipher suites. It is useful for older Android devices or if you cannot connect to your instance. + Exclusively for Peertube videos. Switch this mode if you cannot play them. + These tags will allow to filter statuses from profiles. You will have to use the context menu for seeing them. + Automatically insert a line break after the mention to capitalize the first letter + Allow content creators to share statuses to their RSS feeds + Compose + Maximum retry times when uploading media + Create a new Folder here + Enter the folder name + Please enter a valid folder name + This folder already exists.\n Please provide another name for the folder + Velg + Default Directory + Mappe + Opprett mappe + Display a toast message after an action has been completed (boost, fav, etc.)? + Muted instances have been exported! + Add an instance + Export instances + Import instances + Crash reports + Enable crash reports + If enabled, a crash report will be created locally and then you will be able to share it. + Fedilab har stoppet :( + Du kan sende meg krasjrapporten på e-post. Det vil hjelpe til med å fikse problemet :)\n\nDu kan også legge til ekstra informasjon du mener er nyttig. Tusen takk! + Use the wysiwyg + When enabled, you will be able to format your text easily with tools. + Statistikk + Total statuses + Number of boosts + Number of favourites + Number of mentions + Number of follows + Number of polls + Number of replies + Number of statuses + Statuser + Synlighet + Number with media + Number with sensitive media + Number with CW + First status date + Last status date + First notification date + Last notification date + Hyppighet + %s status per dag + %s meldinger per dag + Date range + Grupper + Ingen grupper! + Disable custom animated emojis + Charts + Display charts + The application collects your local data, please wait… + Sikkerhetskopi + Auto backup statuses + This option is per account. It will launch a service that will automatically store your statuses locally in the database. That allows to get statistics and charts + Auto backup notifications + This option is per account. It will launch a service that will automatically store your notifications locally in the database. That allows to get statistics and charts + Rapporter konto + Send en invitasjon + Your instance does not allow to register a new account! + + %d stemme + %d stemmer + + + %d voter + %d voters + + + Ett valg + Flere valg + + + 5 minutter + 30 minutter + 1 time + 6 timer + 1 dag + 3 dager + 7 dager + + + Torrent + Webvisning + + For joining my instance \"%1$s\", you can download Fedilab:\n\nF-Droid: %2$s\nGoogle: %3$s\n\nThen open the link below with Fedilab and create your account :)\n\n%4$s + + Your poll can\'t have duplicated options! + For alle kontoer + Database cache + Clear your home timeline cache + Clear your cached statuses + Clear your bookmarks + Files in cache + Total notifications + Hide menu items + Fedilab is running live notifications + For %1$s accounts with %2$s events + Live notifications for %1$s + Live notifications will be enabled for this account. + Clear cache when leaving + The cache (media, cached messages, data from the built-in browser) will be automatically cleared when leaving the application. + Do you want to unfollow this account? + Show confirmation dialog before unfollowing + Erstatt Youtube med Invidio.us + Invidious er en alternativ front-end til YouTube + Enter your custom host or leave blank for using invidious.snopyta.org + Bytt ut Twitter med Nitter + Nitter er en åpen kildekode-alternativ Twitter front-end fokusert på personvern. + Enter your custom host or leave blank for using nitter.net + Replace Instagram with Bibliogram + Bibliogram is an open source alternative Instagram front-end focused on privacy. + Enter your custom host or leave blank for using bibliogram.art + Replace Reddit with Libreddit + Libreddit is an open source alternative Reddit front-end focused on privacy. + Enter your custom host or leave blank for using libredd.it + Replace Medium links + Replace medium.com links with an open source alternative front-end focused on privacy. + Default: scribe.rip + Replace Wikipedia links + Replace Wikipedia link with an open source alternative front-end focused on privacy. + Default: wikiless.org + Hide Fedilab notification bar + For hiding the remaining notification in the status bar, tap on the eye icon button then uncheck: \"Display in status bar\" + Use a push notifications system for getting notifications in real time. + No live notifications + Live notifications + Notifications will be fetched every 15 minutes. + Legg til notater + Notater for kontoen + Allow to compress large photos into smaller sized photos with very less or negligible loss in quality of the image. + Allow compressing videos while maintaining their quality. + The app is compressing the media, it can be quite long… + Endre app-ikon + Tap to change the app icon + Legg ut + Visibility of the post + Tap here to add photos + Accepted Formats: jpeg, png, gif \n\nMax File Size: 15 MB \n\nAlbums can contain up to 4 photos or videos + Upload media + Add an optional caption + The app received a very long error message from the API %1$s + Message preview + Add mentions in each message + Fetching conversation + Sortert etter + Title for the video + Join Peertube + I am at least 16 years old and agree to the %1$s of this instance + Lenker + Change the color of links (URLs, mentions, tags, etc.) in messages + Reblogs header + Change the color of display name at the top of messages + Change the color of the user name at the top of messages + Change the color of the header for reblogs + Innlegg + Background color of posts in timelines + Reset colors + Tap here to reset all your custom colors + Tilbakestill + Ikoner + Color of bottom icons in timelines + Pin this tag + Logo of the instance + Rediger profil + Make an action + Oversettelse + Forhåndsvisning av bilde + Tekstfarge + Change the text color in pots + Apply changes + You need to restart the application to apply changes + Start på nytt + Use a custom theme + Allow to override colors of the selected theme above + Theming + Store before + The theme was exported + The theme has been successfully exported in CSV + Apply the primary color to the status bar + Status bar color + Restore a default theme + Import a theme + Tap here to import a theme from a previous export + Export the theme + Tap here to export the current theme + An error occurred when selecting the theme file + Theme Picker + Select a pre-installed theme + Temaer + Apply the primary color to the navigation bar + Navigation bar color + The underlying color of the app’s content. + Background color + Accents select parts of the UI. + Accent color + Displayed most frequently across your app. + Primary color + Export bookmarks to the instance + Import bookmarks from the instance + User count + Status count + Instance count + Blokkert + Slutter om %s + Hva er nytt i %s + You can follow my account for updates + This instance is not available on https://instances.social + Display full link + Del kobling + The URL has been copied to the clipboard + Open with another app + Check redirect + This URL does not redirect + %1$s \n\nredirects to\n\n %2$s + Change the user agent + Set a custom user agent or leave blank + Allows to customize the user agent used for api calls or with the built-in browser. + Remove UTM parameters + The app will automatically remove UTM parameters from URLs before visiting a link. + Trends + Trending now + %d people talking + Twitter accounts (via Nitter) + Twitter usernames space separated + Identity proofs + Verifisert identitet + Verified by %1$s (%2$s) + Delete the notification + Display more options + It is a Pixelfed story + Upload a media, it will be automatically added to your Pixelfed story. + Media successfully added to your story! + Action disabled + Unfollow + Something went wrong, please check your download directory in settings. + Announcements + No announcements! + Add a reaction + Use your favourite browser inside the app. Uncheck this feature to open links externally. + Video cache in MB, zero means no cache. + Watermarks + Automatically add a watermark at the bottom of pictures. The text can be customized for each account. + No distributors found! + You need a distributor for receiving push notifications.\nYou will find more details at %1$s.\n\nYou can also disable push notifications in settings for ignoring that message. + Select a distributor + diff --git a/app/src/main/res/values-oc/strings.xml b/app/src/main/res/values-oc/strings.xml new file mode 100644 index 000000000..9ea0cd62a --- /dev/null +++ b/app/src/main/res/values-oc/strings.xml @@ -0,0 +1,1142 @@ + + + Open the menu + Close the menu + About + About the instance + Privacy + Cache + Logout + Login + + Close + Yes + No + Cancel + Download + Download %1$s + Media saved + File: %1$s + Password + Email + Accounts + Toots + Tags + Save + Restore + No results! + Instance + Instance: mastodon.social + Now works with the account %1$s + Add an account + The content of the toot has been copied to the clipboard + The URL of the toot has been copied to the clipboard + Change + Select a picture… + Clean + Camera + Delete all + Translate this toot. + Schedule + Text and icon sizes + Change the current text size: + Change the current icon size: + Next + Previous + Open with + Validate + Media + Share with + Shared via Fedilab + Replies + User name + Drafts + Favourites + New followers + Mentions + Boosts + Show boosts + Show replies + Open in browser + Translate + Please, wait few seconds before making this action. + + Home + Local timeline + Federated timeline + Options + Favourites + Communication + Muted users + Blocked users + Notifications + Follow requests + Settings + Remove an account + Remove the account %1$s from the application? + Send an email + Tap on the path to change it + Failed! + Scheduled toots + Information below may reflect the user\'s profile incompletely. + Insert emoji + The app did not collect custom emojis for the moment. + Push notifications + Are you sure you want to logout? + Are you sure you want to logout @%1$s@%2$s? + + No toot to display + No stories to display + Stories + Boosted by %1$s + Add this toot to your favourites? + Remove this toot from your favourites? + Boost this toot? + Unboost this toot? + Pin this toot? + Unpin this toot? + Mute + Block + Report + Delete + Copy + Share + Mention + Timed mute + Delete & re-draft + + Mute this account? + Block this account? + Report this toot? + Block this domain? + Unmute this account? + Unblock this account? + + + Notify + Silent + + + Delete this toot? + Delete & re-draft this toot? + + Bookmarks + Add to bookmarks + Remove bookmark + No bookmarks to display + Status has been added to bookmarks! + Status was removed from bookmarks! + + %d s + %d m + %d h + %d d + + %d second + %d seconds + + + %d minute + %d minutes + + + %d hour + %d hours + + + %d day + %d days + + + Warning + What is on your mind? + TOOT! + QUEET! + cw + Write a toot + Reply to a toot + Write a queet + Reply to a queet + Select a media + An error occurred while selecting the media! + Remove this media? + Your toot is empty! + Visibility of the toot + Visibility of the toots by default: + The toot has been sent! + You are replying to this toot: + Sensitive content? + + Post to public timelines + Do not post to public timelines + Post to followers only + Post to mentioned users only + + No drafts! + Choose a toot + Choose an account + Select some accounts + Delete draft? + Tap on the button to display the original toot + Describe for the visually impaired + + No description available! + + Release %1$s + Developer: + License: + GNU GPL V3 + Source code: + Translation of toots: + Search instances: + Icon designer: + + Conversation + + No account to display + No follow request + Toots \n %1$s + Following \n %1$s + Followers \n %1$s + Pinned \n %d + Authorize + Reject + + No scheduled toots to display! + Write a toot and then choose Schedule from the top menu. + Delete scheduled toot? + Media: %d + The toot has been scheduled! + The scheduled date must be greater than the current hour! + Battery saver is enabled! It might not work as expected. + + The time for muting should be greater than one minute. + %1$s has been muted until %2$s.\n You can unmute this account from their profile page. + %1$s is muted until %2$s.\n Tap here to unmute the account. + + No notification to display + mentioned you + wrote a new message + boosted your status + favourited your status + followed you + asked to follow you + + and another notification + and %d other notifications + + + %d like + %d likes + + Delete a notification? + Delete all notifications? + The notification has been deleted! + All notifications have been deleted! + + Following + Followers + Pinned + + Unable to get client id! + Unable to connect to instance domain! + No Internet connection! + The account was blocked! + The account is no longer blocked! + The account was muted! + The account is no longer muted! + The account was followed! + The account is no longer followed! + The toot was boosted! + The toot is no longer boosted! + The toot was added to your favourites! + The toot was removed from your favourites! + The toot was reported! + The toot was deleted! + The toot was pinned! + The toot was unpinned! + Oops ! An error occurred! + An error occurred! The instance did not return an authorisation code! + The instance domain does not seem to be valid! + An error occurred while switching between accounts! + An error occurred while searching! + The profile data have been saved! + No action can be taken + The media has been saved! + An error occurred while translating! + Translations are disabled in settings + Draft saved! + Are you sure this instance allows this number of characters? Usually, this value is close to 500 characters. + Visibility of the toots has been changed for the account %1$s + + Number of toots per load + Always + WIFI + Ask + Load the media + Load the pictures + Show more… + Show less… + Sensitive content + Disable GIF avatars + Path: + Save drafts automatically + Add URL of media in toots + Notify when someone follows you + Notify when someone boosts your status + Notify when someone favourites your status + Notify when someone mentions you + Notify when a poll ended + Notify for new posts + Show confirmation dialog before boosting + Show confirmation dialog before adding to favourites + Notify in WIFI only + Notify? + Silent Notifications + NSFW view timeout (seconds, 0 means off) + Media Description timeout (seconds, 0 means off) + Edit profile + Custom sharing + Your custom sharing URL… + Bio… + Lock account + Save changes + Choose a header picture + Fit preview images + Automatically split toots in replies when chars are over: + You have reached the 160 characters allowed! + You have reached the 30 characters allowed! + Between + and + The time must be greater than %1$s + The time must be lower than %1$s + Start time + End time + Use the built-in browser + Custom tabs + Enable Javascript + Automatically expand cw + Allow third-party cookies + Your API key, you can leave blank for Yandex + + Dark + Light + Black + + Set LED colour: + + Blue + Cyan + Magenta + Green + Red + Yellow + White + + Follow + Unblock + Mute + Unmute + Request sent + Follows you + Search + First letter in capital for replies + Resize pictures + Resize videos + + Push notifications + Please, confirm push notifications that you want to receive. + You can enable or disable these notifications later in settings (Notifications tab). + + + Clear cache + There are %1$s of data in cache.\n\nWould you like to delete them? + Mb + Cache was cleared! %1$s were released + + Title + Title… + Description + Keywords + Keywords… + + Synchronize + Filter + Your toots + Your notifications + Public + Unlisted + Private + Direct + Some keywords… + Show media + Show pinned + No matching result found! + Backup toots for %1$s + %1$s new toots have been imported + %1$s new notifications have been imported + + Dates descending + Dates ascending + + + No + Only + Both + + No toots were found in database. Please, use the synchronize button from the menu to retrieve them. + + Recorded data + Only basic information from accounts are stored on the device. + These data are strictly confidential and can only be used by the application. + Deleting the application immediately removes these data.\n + ⚠ Login and passwords are never stored. They are only used during a secure authentication (SSL) with an instance. + + Permissions: + - ACCESS_NETWORK_STATE: Used to detect if the device is connected to a WIFI network.\n + - INTERNET: Used for queries to an instance.\n + - WRITE_EXTERNAL_STORAGE: Used to store media or to move the app on a SD card.\n + - READ_EXTERNAL_STORAGE: Used to add media to toots.\n + - BOOT_COMPLETED: Used to start the notification service.\n + - WAKE_LOCK: Used during the notification service. + + API permissions: + - Read: Read data.\n + - Write: Post statuses and upload media for statuses.\n + - Follow: Follow, unfollow, block, unblock.\n\n + ⚠ These actions are carried out only when user requests them. + + Tracking and Libraries + The application does not use tracking tools (audience measurement, error reporting, etc.) and does not contain any advertising.\n\n + The use of libraries is minimized: \n + - Glide: To manage media\n + - Android-Job: To manage services\n + - PhotoView: To manage images\n + + Translation of toots + The application offers the ability to translate toots using the locale of the device and the Yandex API.\n + Yandex has its proper privacy-policy which can be found here: https://yandex.ru/legal/confidential/?lang=en + + Thank you to: + + Filter out by regular expressions + Search + Delete + Fetch more toots… + + Lists + Are you sure you want to permanently delete this list? + There is nothing in this list yet. When members of this list post new statuses, they will appear here. + Add to list + Add list + Delete list + Edit list + New list title + The account was added to the list! + You don\'t have any lists yet! + + %1$s has moved to %2$s + Authentication does not work? + Here are some checks that might help:\n\n + - Check there is no spelling mistakes in the instance name\n\n + - Check that your instance is not down\n\n + - If you use the two-factor authentication (2FA), please use the link at the bottom (once the instance name is filled)\n\n + - You can also use this link without using the 2FA\n\n + - If it still does not work, please raise an issue on FramaGit at https://framagit.org/tom79/fedilab/issues + + Media has been loaded. Tap here to display it. + This action can be quite long. You will be notified when it will be finished. + Still running, please wait… + Export statuses + Export statuses for %1$s + %1$s toots out of %2$s have been exported. + Something went wrong when exporting data for %1$s + Something went wrong when exporting data! + Something went wrong when importing data! + + Proxy + Enable proxy? + Host + Port + Login + Password + Add toot details when sharing + Support the app on Liberapay + There is an error in the regular expression! + No timelines was found on this instance! + Delete this instance? + Translate in + Follow instance + You already follow this instance! + The instance is followed! + Partnerships + Information + Hide boosts from %s + Feature on profile + Show boosts from %s + Don\'t feature on profile + The account is now featured on profile + The account is no longer featured on profile + Boosts are now shown! + Boosts are now hidden! + Direct message + Filters + No filters to display. You can create one by tapping on the \"+\" button. + Keyword or phrase + Home timeline + Public timelines + Notifications + Conversations + Will be matched regardless of casing in text or content warning of a toot + Drop instead of hide + Filtered toots will disappear irreversibly, even if filter is later removed + When the keyword or phrase is alphanumeric only, it will only be applied if it matches the whole word + Whole word + Filter contexts + One or multiple contexts where the filter should apply + Expire after + Delete filter? + Update filter + Create filter + Whom to follow + There is no accounts listed for the moment! + Follow + Select all + Unselect all + %s is followed! + Creating the list %s + Adding accounts to the list + Accounts were added to the list + Adding accounts to the list + You have not created a list yet. Tap on the \"+\" button to add a new one. + Who to follow + Trunk API + Account(s) can\'t be followed + Fetching remote account + Automatically expand hidden media + New follow + New Boost + New Favourite + New Mention + Poll Ended + New Toot + Toots Backup + New posts + Media Download + Change notification sound + Select Tone + Enable time slot + How To Videos + Fetching remote thread! + No blocked domains! + Unblock domain + Are you sure to unblock %s? + Are you sure to block %s?\n\nYou will not see any content from that domain in any public timeline or in your notifications. Your followers from that domain will be removed. + Blocked domains + Block domain + The domain is blocked + The domain is no longer blocked! + Fetching remote status + Comment + Peertube instance + Be the first to leave a comment on this video with the top right button! + %s views + Duration: %s + Add an instance + Comments are not enabled on this video! + Pick up a resolution + Peertube favourites + The video has been added to bookmarks! + The video has been removed from bookmarks! + There is no Peertube videos in your favourites! + Channel + Videos + Channels + Use Emoji One + Information + Display previews in all toots + New UX/UI designer + Display video previews + The account id has been copied in the clipboard! + Change the language + Default language + Truncate long toots + Truncate toots over \'x\' lines. Zero means disabled. + Display more + Display less + Manage tags + The tag already exists! + The tag has been stored! + The tag has been changed! + The tag has been removed! + Schedule boost + The boost is scheduled! + No scheduled boost to display! + Schedule boost.]]> + Art timeline + Open menu + Go back + Logo of the application + Profile picture + Profile banner + Contact admin of the instance + Add new + MastoHost logo + Emoji picker + Refresh + Expand the conversation + Remove an account + Remove the blocked domain + Custom emoji picker + Play video + New toot + Image of the card + Hide media + Favicon + Add description for media (for the visually impaired) + + Never + 30 minutes + 1 hour + 6 hours + 12 hours + 1 day + 1 week + + In this field, you need to write your instance host name.\nFor example, if you created your account on https://mastodon.social\nJust write mastodon.social (without https://)\n + You can start writing first letters and names will be suggested.\n\n + ⚠ The Login button will only work if the instance name is valid and the instance is up! + + More information + + Languages + Media only + Show NSFW + Crowdin translations + Crowdin manager + Translation of the application + About Crowdin + Bot + Pixelfed instance + Mastodon instance + Any of these + All of these + None of these + Any of these words (space-separated) + All these words (space-separated) + Add some words to filter (space-separated) + Change column name + Misskey instance + No app supporting this link is installed on your device. + Subscriptions + Overview + Trending + Recently added + Local + Upload + Reply + Delete a comment + Are you sure to delete this comment? + Full screen video + Mode for videos + Select the file to upload + My videos + Title + License + Category + Language + This video contains mature or explicit content + Enable video comments + Update video + Description + The video has been updated! + Upload cancelled! + The video has been uploaded! + Uploading, please wait… + Tap here to edit the video data. + Delete video + Are you sure to delete this video? + Display NSFW videos + No videos to display! + Leave a comment + Share + Choose a schedule mode + From device + From server + Toots (Server) + Toots (Device) + Modify + Display new toots above the \"Fetch more\" button + Timelines + Interface + Contacts + %1$s commented your video %2$s]]> + %1$s is following your channel %2$s]]> + %1$s is following your account]]> + %1$s has been published]]> + %1$s succeeded]]> + %1$s failed]]> + %1$s published a new video: %2$s]]> + %1$s has been blacklisted]]> + %1$s has been unblacklisted]]> + Export data + Import Data + Select the file to import + An error occurred when selecting the backup file! + Add a public comment + Send comment + There is no Internet connection. Your message has been stored in drafts. + Plain text + HTML + Markdown + Logout account + All + Support the app + Open Collective enables groups to quickly set up a collective, raise funds and manage them transparently. + Copy link + Connect + Normal + Compact + Console + Set display mode + Patch the Security Provider + Update tracking domains + The tracking data base has been updated! + http calls blocked by the application + List of blocked calls + Submit + The data base has been exported! + Featured hashtags + Filter timeline with tags + No tags + Hide the \"delete\" button in the notification tab + Attach an image when sharing a URL + + Poll + Polls + Create a poll + Choice 1 + Choice 2 + Choice %d + You need two choices at least for the poll! + Done + end at %s + Refresh poll + Vote + A poll you have voted in has ended + A poll you tooted has ended + Customize + Categories + Time slot + Advanced + Display \'new\' badge on unread toots + Peertube + Move timeline + Hide timeline + Reorder timelines + List permanently deleted + Followed instance removed + Pinned tag removed + Undo + You need to keep two visible tabs! + Reorder timelines + Main timelines can only be hidden! + BBCode + Always mark media as sensitive + GNU instance + Cached status + Forward tags in replies + Long press to store media + Blur sensitive media + Display timelines in a list + Display timelines + Mark bot accounts in toots + Manage tags + Remember the position in Home timeline + History + Playlists + Display name + You don\'t have any playlists. Tap on the \"+\" icon to add a new playlist + You must provide a display name! + The channel is required when the playlist is public. + Create a playlist + There is nothing in this playlist yet. + redo + Gallery + Emoji + Sticker + Eraser + Text + Filter + Brush + Are you sure you want to exit without saving the image? + Discard + Saving… + Image Saved Successfully! + Failed to save Image + Opacity + Enable photo editor + Add a poll item + Remove last poll item + Mute conversation + Unmute conversation + The conversation is no longer muted! + The conversation is muted + Open application features + Timed mute + Mention the account + Refresh cache + Mention the status + News + General + Regional + Art + Journalism + Activism + Gaming + Technology + Adult content + Furry + Food + Logo of the instance + Something went wrong when checking available instances! + Join Mastodon + Choose an instance by picking up a category, then tap on a check button. + Choose an instance by tapping on a check button. + %1$s users + Confirm password + I agree to %1$s and %2$s + server rules + terms of service + Sign up + This instance works with invitations. Your account will need to be manually approved by an administrator before being usable. + Please, fill all the fields! + Passwords don\'t match! + The email doesn\'t seem to be valid! + Your username will be unique on %1$s + You will be sent a confirmation e-mail + Use at least 8 characters + Password should contain at least 8 characters + Username should only contain letters, numbers and underscores + Account created! + Your account has been created!\n\n + Think to validate your email within the 48 next hours.\n\n + You can now connect your account by writing %1$s in the first field and tap on Connect.\n\n + Important: If your instance required validation, you will receive an email once it is validated! + + Save the message in drafts? + Administration + Reports + No reports to display! + Reconnect the account + The application failed to access the admin features. You might need to reconnect the account for having the correct scope. + Unresolved + Remote + Active + Pending + Disabled + Silenced + Suspended + Permissions + Email status + Login status + Joined + Most recent IP + Warn + Disable + Silence + Notify the user per e-mail + Custom warning + User + Moderator + Administrator + Confirmed + Not confirmed + Reported statuses + Account + Undo silence + Undo disable + Suspend + Undo suspend + The account is silenced! + The account is no longer silenced! + The account is suspended! + The account is no longer suspended! + The account is disabled! + The account is no longer disabled! + The account has been warned! + Display the admin menu + Display the admin feature in statuses + Allow + The account is approved! + The account is rejected! + Assign to me + Unassign + Mark as resolved + Mark as unresolved + Empty content! + Display Fedilab features button + The application needs to access audio recording + Voice message + Enable quick reply + The account you are replying might not see your message! + If disabled, the app will always load last statuses + If disabled, sensitive media will be hidden with a button + Store media in full size with a long press on previews + Add an ellipse button at the top right for listing all tags/instances/lists + During the time slot, the app will send notifications. You can reverse (ie: silent) this time slot with the right spinner. + Display a Fedilab button below profile picture. It is a shortcut for accessing in-app features. + Allow to reply directly in timelines below statuses + Previews will not be cropped in timelines + Allow to play embedded videos directly in timelines + Allow to reverse the way to read statuses that are displayed once tapping the fetch more button + This option allows to support recent cipher suites. It is useful for older Android devices or if you cannot connect to your instance. + Exclusively for Peertube videos. Switch this mode if you cannot play them. + These tags will allow to filter statuses from profiles. You will have to use the context menu for seeing them. + Automatically insert a line break after the mention to capitalize the first letter + Allow content creators to share statuses to their RSS feeds + Compose + Maximum retry times when uploading media + Create a new Folder here + Enter the folder name + Please enter a valid folder name + This folder already exists.\n Please provide another name for the folder + Select + Default Directory + Folder + Create folder + Display a toast message after an action has been completed (boost, fav, etc.)? + Muted instances have been exported! + Add an instance + Export instances + Import instances + Crash reports + Enable crash reports + If enabled, a crash report will be created locally and then you will be able to share it. + Fedilab has stopped :( + You can send me by email the crash report. It will help to fix it :)\n\nYou can add additional content. Thank you! + Use the wysiwyg + When enabled, you will be able to format your text easily with tools. + Statistics + Total statuses + Number of boosts + Number of favourites + Number of mentions + Number of follows + Number of polls + Number of replies + Number of statuses + Statuses + Visibility + Number with media + Number with sensitive media + Number with CW + First status date + Last status date + First notification date + Last notification date + Frequency + %s statuses per day + %s notifications per day + Date range + Groups + No groups! + Disable custom animated emojis + Charts + Display charts + The application collects your local data, please wait… + Backup + Auto backup statuses + This option is per account. It will launch a service that will automatically store your statuses locally in the database. That allows to get statistics and charts + Auto backup notifications + This option is per account. It will launch a service that will automatically store your notifications locally in the database. That allows to get statistics and charts + Report account + Send an invitation + Your instance does not allow to register a new account! + + %d vote + %d votes + + + %d voter + %d voters + + + Single choice + Multiple choices + + + 5 minutes + 30 minutes + 1 hour + 6 hours + 1 day + 3 days + 7 days + + + Webview + Direct stream + + For joining my instance \"%1$s\", you can download Fedilab:\n\nF-Droid: %2$s\nGoogle: %3$s\n\nThen open the link below with Fedilab and create your account :)\n\n%4$s + + Your poll can\'t have duplicated options! + For all accounts + Database cache + Clear your home timeline cache + Clear your cached statuses + Clear your bookmarks + Files in cache + Total notifications + Hide menu items + Fedilab is running live notifications + For %1$s accounts with %2$s events + Live notifications for %1$s + Live notifications will be enabled for this account. + Clear cache when leaving + The cache (media, cached messages, data from the built-in browser) will be automatically cleared when leaving the application. + Do you want to unfollow this account? + Show confirmation dialog before unfollowing + Replace Youtube with Invidio.us + Invidious is an alternative front-end to YouTube + Enter your custom host or leave blank for using invidious.snopyta.org + Replace Twitter with Nitter + Nitter is an open source alternative Twitter front-end focused on privacy. + Enter your custom host or leave blank for using nitter.net + Replace Instagram with Bibliogram + Bibliogram is an open source alternative Instagram front-end focused on privacy. + Enter your custom host or leave blank for using bibliogram.art + Replace Reddit with Libreddit + Libreddit is an open source alternative Reddit front-end focused on privacy. + Enter your custom host or leave blank for using libredd.it + Replace Medium links + Replace medium.com links with an open source alternative front-end focused on privacy. + Default: scribe.rip + Replace Wikipedia links + Replace Wikipedia link with an open source alternative front-end focused on privacy. + Default: wikiless.org + Hide Fedilab notification bar + For hiding the remaining notification in the status bar, tap on the eye icon button then uncheck: \"Display in status bar\" + Use a push notifications system for getting notifications in real time. + No live notifications + Live notifications + Notifications will be fetched every 15 minutes. + Add notes + Notes for the account + Allow to compress large photos into smaller sized photos with very less or negligible loss in quality of the image. + Allow compressing videos while maintaining their quality. + The app is compressing the media, it can be quite long… + Change app icon + Tap to change the app icon + Post + Visibility of the post + Tap here to add photos + Accepted Formats: jpeg, png, gif \n\nMax File Size: 15 MB \n\nAlbums can contain up to 4 photos or videos + Upload media + Add an optional caption + The app received a very long error message from the API %1$s + Message preview + Add mentions in each message + Fetching conversation + Order by + Title for the video + Join Peertube + I am at least 16 years old and agree to the %1$s of this instance + Links + Change the color of links (URLs, mentions, tags, etc.) in messages + Reblogs header + Change the color of display name at the top of messages + Change the color of the user name at the top of messages + Change the color of the header for reblogs + Posts + Background color of posts in timelines + Reset colors + Tap here to reset all your custom colors + Reset + Icons + Color of bottom icons in timelines + Pin this tag + Logo of the instance + Edit profile + Make an action + Translation + Image preview + Text color + Change the text color in pots + Apply changes + You need to restart the application to apply changes + Restart + Use a custom theme + Allow to override colors of the selected theme above + Theming + Store before + The theme was exported + The theme has been successfully exported in CSV + Apply the primary color to the status bar + Status bar color + Restore a default theme + Import a theme + Tap here to import a theme from a previous export + Export the theme + Tap here to export the current theme + An error occurred when selecting the theme file + Theme Picker + Select a pre-installed theme + Themes + Apply the primary color to the navigation bar + Navigation bar color + The underlying color of the app’s content. + Background color + Accents select parts of the UI. + Accent color + Displayed most frequently across your app. + Primary color + Export bookmarks to the instance + Import bookmarks from the instance + User count + Status count + Instance count + Blocked + End in %s + What\'s new in %s + You can follow my account for updates + This instance is not available on https://instances.social + Display full link + Share link + The URL has been copied to the clipboard + Open with another app + Check redirect + This URL does not redirect + %1$s \n\nredirects to\n\n %2$s + Change the user agent + Set a custom user agent or leave blank + Allows to customize the user agent used for api calls or with the built-in browser. + Remove UTM parameters + The app will automatically remove UTM parameters from URLs before visiting a link. + Trends + Trending now + %d people talking + Twitter accounts (via Nitter) + Twitter usernames space separated + Identity proofs + Verified identity + Verified by %1$s (%2$s) + Delete the notification + Display more options + It is a Pixelfed story + Upload a media, it will be automatically added to your Pixelfed story. + Media successfully added to your story! + Action disabled + Unfollow + Something went wrong, please check your download directory in settings. + Announcements + No announcements! + Add a reaction + Use your favourite browser inside the app. Uncheck this feature to open links externally. + Video cache in MB, zero means no cache. + Watermarks + Automatically add a watermark at the bottom of pictures. The text can be customized for each account. + No distributors found! + You need a distributor for receiving push notifications.\nYou will find more details at %1$s.\n\nYou can also disable push notifications in settings for ignoring that message. + Select a distributor + diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml new file mode 100644 index 000000000..4732d3368 --- /dev/null +++ b/app/src/main/res/values-pl/strings.xml @@ -0,0 +1,1155 @@ + + + Otwórz menu + Zamknij menu + O Fedilab + O instancji + Prywatność + Pamięć podręczna + Wyloguj + Zaloguj + + Zamknij + Tak + Nie + Anuluj + Pobierz + Pobierz %1$s + Pliki zapisane + Plik: %1$s + Hasło + Email + Konta + Wpisy + Tagi + Zapisz + Przywracanie + Brak wyników! + Instancja + Instancja: mastodon.social + Teraz pracuje z kontem %1$s + Dodaj konto + Zawartość wpisu została skopiowana do schowka + Zawartość wpisu została skopiowana do schowka + Zmień + Wybierz zdjęcie… + Czyste + Kamera + Usuń wszystko + Przetłumacz ten wpis. + Harmonogram + Rozmiary tekstu i ikon + Zmień rozmiar tekstu: + Zmień rozmiar ikon: + Następne + Poprzednie + Otwórz za pomocą + Zatwierdź + Media + Udostępnij + Udostępniony przez Fedilab + Odpowiedzi + Nazwa użytkownika + Szkice + Ulubione + Nowi śledzący + Wspomnienia + Podbicia + Pokaż podbicia + Pokaż odpowiedzi + Otwórz w przeglądarce + Przetłumacz + Proszę poczekać kilka sekund przed wykonaniem tej akcji. + + Strona główna + Lokalna oś czasu + Globalna oś czasu + Opcje + Ulubione + Komunikacja + Wyciszeni użytkownicy + Zablokowani użytkownicy + Powiadomienia + Prośby o śledzenie + Ustawienia + Usuń konto + Usunąć konto %1$s z aplikacji? + Wyślij email + Kliknij na ścieżkę, aby ją zmienić + Niepowodzenie! + Zaplanowane wpisy + Poniższe informacje mogą nie odzwierciedlać w pełni profilu użytkownika. + Wstaw emoji + Brak niestandardowych emoji. + Powiadomienia push + Czy na pewno chcesz się wylogować? + Czy na pewno chcesz się wylogować z konta @%1$s@%2$s? + + Brak wpisów do wyświetlenia + No stories to display + Stories + Podbity przez %1$s + Dodać ten wpis do ulubionych? + Usunąć ten wpis z ulubionych? + Podbić ten wpis? + Anulować podbicie? + Przypiąć ten wpis? + Odpiąć ten wpis? + Wycisz + Zablokuj + Zgłoś + Usuń + Kopiuj + Udostępnij + Wspomnij + Czasowe wyciszenie + Usuń i przeredaguj + + Wyciszyć? + Zablokować? + Zgłosić ten wpis? + Zablokować tę domenę? + Cofnąć wyciszenie tego konta? + Odblokować to konto? + + + Powiadomienie + Cichy + + + Usunąć ten wpis? + Usunąć i przeredagować ten wpis? + + Zakładki + Dodaj do zakładek + Usuń zakładkę + Brak zakładek + Dodano wpis do zakładek! + Usunięto wpis z zakładek! + + %d s + %d min + %d godz. + %d dni + + %d second + %d seconds + %d seconds + %d sekundd + + + %d minuta + %d minuty + %d minut + %d minut + + + %d godzina + %d godziny + %d godzin + %d godzin + + + %d dzień + %d dni + %d dni + %d dni + + + Ostrzeżenie + Co Ci chodzi po głowie? + Wyślij! + Wyślij! + cw + Nowy wpis + Odpowiedz na wpis + Nowy wpis + Odpowiedz na wpis + Wybierz zawartość + Podczas wyboru zawartości wystąpił błąd! + Usunąć tą zawartość? + Twój wpis jest pusty! + Widoczność wpisu + Domyślna widoczność wpisu: + Wpis został wysłany! + Odpowiadasz na ten wpis: + Wrażliwe treści? + + Wyświetlaj na publicznej osi czasu + Nie wyświetlaj na publicznych osiach czasu + Wyślij tylko do śledzących + Wyślij tylko do wspomnianych użytkowników + + Nie znaleziono szkiców! + Wybierz wpis + Wybierz konto + Wybierz wiele kont + Usunąć szkic? + Kliknij przycisk, aby wyświetlić oryginalny wpis + Opis dla niedowidzących + + Brak dostępnego opisu! + + Wersja %1$s + Twórca: + Licencja: + GNU GPL V3 + Kod źródłowy: + Tłumaczenie wpisów: + Więcej o instancjach: + Projektant ikon: + + Rozmowa + + Brak kont do wyświetlenia + Brak próśb o śledzenie + Wpis \n %1$s + Śledzeni \n %1$s + Śledzący \n %1$s + Przypięte \n %d + Autoryzuj + Odrzuć + + Brak zaplanowanych wpisów! + Utwórz wpis i wybierz harmonogram z górnego menu. + Usunąć zaplanowany wpis? + Zawartość w liczbie %d + Wpis został zaplanowany! + Zaplanowana data musi być większa niż bieżąca godzina! + Włączony jest tryb oszczędzania baterii! To może nie działać zgodnie z oczekiwaniami. + + Czas wyciszenia musi być większy niż jedna minuta. + %1$s został wyciszony do %2$s.\n Możesz to anulować na ekranie podglądu konta. + %1$s został wyciszony do %2$s.\n Kliknij tutaj, aby anulować. + + Brak powiadomień do wyświetlenia + wspomniał o tobie + napisał nową wiadomość + podbił twój wpis + dodał(a) Twój wpis do ulubionych + zaczął(-ęła) Cię śledzić + poprosił o obserwowanie + + dodaj %d innych powiadomień + dodaj %d innych powiadomień + dodaj %d innych powiadomień + dodaj %d innych powiadomień + + + %d polubienie + %d polubienia + %d polubień + %d polubień + + Czy na pewno chcesz usunąć to powiadomienie? + Wyczyścić wszytstkie powiadomienia? + Usunięto powiadomienie! + Wyczyszczono powiadomienia! + + Śledzenie + Śledzący + Przypięte + + Nie można pobrać identyfikatora klienta! + Nie można połączyć się z domeną instancji! + Brak połączenia z Internetem! + Konto zostało zablokowane! + Konto nie jest już zablokowane! + Konto zostało wyciszone! + Konto nie jest już wyciszone! + Zaczęto śledzić konto! + Konto nie jest już śledzone! + Podbito wpis! + Cofnięto podbicie wpisu! + Wpis został dodany do ulubionych! + Wpis został usunięty z ulubionych! + Zgłoszono wpis! + Usunięto wpis! + Przypięto wpis! + Odpięto wpis! + Ups! Wystąpił błąd! + Wystąpił błąd! Instancja nie zwróciła kodu autoryzacji! + Domena instancji jest niepoprawna! + Podczas zmiany kont wystąpił błąd! + Podczas wyszukiwania wystąpił błąd! + Dane profilu zostały zapisane! + Żadna akcja nie może zostać podjęta + Zapisano zawartość multimedialną! + Tłumaczenie nieudane! + Tłumaczenia są wyłączone w ustawieniach + Zapisano szkic! + Czy na pewno ta instancja zezwala na więcej, niż domyślne 500 znaków? + Widoczność wpisu została zmieniona na %1$s + + Liczba wpisów do załadowania + Zawsze + WiFi + Zapytaj + Załaduj zawartość + Załaduj zawartość + Pokaż więcej… + Pokaż mniej… + Treść wrażliwa + Wyłącz animowane obrazy profilowe + Ścieżka: + Automatyczny zapis szkicu + Dodaj adres URL zawartości we wpisach + Powiadom mnie, gdy ktoś zaczyna mnie śledzić + Powiadom mnie, gdy ktoś podbije mój wpis + Powiadom mnie, gdy ktoś polubi mój wpis + Powiadom mnie, gdy ktoś mnie wspomni + Powiadom kiedy głosowanie zostanie zakończone + Powiadom o nowych postach + Proś o potwierdzenie przed podbiciem + Proś o potwierdzenie przed dodaniem wpisu do ulubionych + Powiadamiaj tylko w sieci WiFi + Powiadamiać? + Ciche powiadomienia + Czas do automatycznego ukrycia treści wrażliwej w sekundach. 0 oznacza dezaktywację. + Media Description timeout (seconds, 0 means off) + Edytuj profil + Niestandardowe udostępnianie + Twoje URL do niestandardowego udostępniania… + Biografia… + Zablokuj konto + Zapisz zmiany + Wybierz zdjęcie nagłówka + Pokaż całość + Automatycznie rozdzielaj wpisy na ponad 500 znaków w odpowiedziach + Przekroczono limit 160 dozwolonych znaków! + Przekroczono limit 30 dozwolonych znaków! + Między + i + Należy ustawić wcześniejszy, niż %1$s + Należy ustawić późniejszy, niż %1$s + Godzina rozpoczęcia + Czas zakończenia + Korzystaj z wbudowanej przeglądarki + Własne zakładki + Włącz Javascript + Zawsze rozwijaj ostrzeżenia o zawartości + Zezwalaj na pliki cookie firm trzecich + Klucz API, można zostawić puste dla Yandex + + Ciemny + Jasny + Czarny + + Ustaw kolor diody LED: + + Niebieski + Błękitny + Purpurowy + Zielony + Czerwony + Żółty + Biały + + Śledź + Odblokuj + Wycisz + Wycisz + Wysłano prośbę. + Śledzi Cię + Wyszukaj + Zaczynaj wpisy z wielkiej litery + Zmniejszanie zdjęć + Zmień rozmiar wideo + + Powiadomienia + Wskaż, jakie powiadomienia chcesz otrzymywać. + Możesz włączyć i wyłączyć te powiadomienia w ustawieniach powiadomień. + + Wyczyść pamięć podręczną + Znaleziono %1$s danych w pamięci podręcznej.\n\nCzy chcesz je usunąć? + Mb + Pamięć podręczna została wyczyszczona! Zwolniono %1$s + + Tytuł + Tytuł… + Opis + Słowa kluczowe + Słowa kluczowe… + + Synchronizacja + Filtrowanie + Twoje wpisy + Twoje powiadomienia + Publiczne + Niepubliczne + Tylko śledzeni + Bezpośrednio + Słowa kluczowe… + Pokaż zawartość + Pokaż przypięte + Nie znaleziono pasujących wpisów! + Kopia zapasowa dla %1$s + Zaimportowano %1$s nowych wpisów + Zaimportowano %1$s nowych powiadomień + + Data malejąco + Data rosnąco + + + Żaden + Jeden + Oba + + Nie znaleziono wpisów. Proszę użyć funkcji synchronizacji w celu pobrania nowych wpisów. + + Przechowywane dane + Tylko podstawowe informacje o koncie są przechowywane na urządzeniu. + Te dane nie są dostępne dla innych aplikacji. + Dezinstalacja aplikacji natychmiastowo usuwa wszystkie dane.\n + ⚠ Dane logowania nie są przechowywane na urządzeniu. Są użyte tylko podczas pierwszego logowania i przekazywane do instancji szyfrowanym kanałem SSL. + + Uprawnienia: + - ACCESS_NETWORK_STATE: Pozwala sprawdzić, czy urządzenie jest połączone z siecią WiFi.\n + - INTERNET: Pozwala na połączenie z instancją.\n + - WRITE_EXTERNAL_STORAGE: Pozwala na zachowywanie multimediów i przeniesienie aplikacji na kartę SD.\n + - READ_EXTERNAL_STORAGE: Pozwala dodać zawartość do wpisów.\n + - BOOT_COMPLETED: Pozwala wyświetlać powiadomienia po włączeniu urządzenia.\n + - WAKE_LOCK: Pozwala wyświetlać powiadomienia w tle. + + Uprawnienia API: + - Odczyt: Możliwość odczytu danych.\n + - Zapis: Wysyłanie wpisów i załączanie multimediów.\n + - Śledzenie: Śledzenie i blokowanie.\n\n + ⚠ Te uprawnienia są wykorzystywane wyłącznie na Twoje żądanie. + + Śledzenie i biblioteki + Fedilab nie śledzi Twojej aktywności (zbieranie wrażliwych danych, zgłaszanie błędów) i nie zawiera reklam.\n\n + Używane biblioteki: \n + - Glide: zarządzanie zawartością multimedialną\n + - Android-Job: zarządzanie usługami\n + - PhotoView: zarządzanie obrazami\n + + Automatyczne tłumaczenie wpisów: + Fedilab oferuje automatyczne tłumaczenie wpisów na wybrany język przy pomocy API Yandex.\n + Polityka prywatności usługi Yandex jest dostępna w języku angielskim tutaj: https://yandex.ru/legal/confidential/?lang=en + + Podziękowania dla: + Filtruj przez wyrażenia regularne + Szukaj + Usuń + Pobierz więcej wpisów… + + Listy + Czy na pewno chcesz usunąć tę listę? Nie będzie można cofnąć tej akcji! + Nie ma jeszcze nic na tej liście. Kiedy użytkownicy z tej listy dodadzą nowe statusy, wyświetlą się one tutaj. + Dodaj do listy + Dodaj listę + Usuń listę + Edytuj listę + Nowy tytuł listy + Konto zostały dodane do listy! + Nie masz jeszcze żadnych list! + + %1$s przeniósł się do %2$s + Problem z uwierzytelnieniem? + Spróbuj zastosować się do poniższych rad:\n\n + - Sprawdź, czy nazwa instancji została poprawnie przepisana\n\n + - Sprawdź, czy instancja jest dostępna\n\n + - Jeśli używasz dwustopniowego uwierzytelniania (2FA), kliknij w link poniżej (po wpisaniu nazwy instancji)\n\n + - Możesz użyć tego linku nawet gdy nie używasz 2FA\n\n + - Jeśli wciąż nie możesz się zalogować, proszę zgłoś problem tutaj: https://framagit.org/tom79/fedilab/issues + + Załadowano zawartość multimedialną. Naciśnij tutaj, aby ją wyświetlić. + To działanie może trochę potrwać. Powiadomimy cię o jego zakończeniu. + Wciąż pracuję, proszę czekać… + Eksportuj wpisy + Eksportuj wpisy dla %1$s + Wyeksportowano %1$s wpisów z dostępnych %2$s. + Coś poszło nie tak podczas próby eksportu danych %1$s + Coś poszło nie tak podczas próby eksportu danych! + Coś poszło nie tak podczas próby importu danych! + + Proxy + Włączyć proxy? + Host + Port + Login + Hasło + Dodaj szczegóły wpisu podczas udostępniania + Wesprzyj tą aplikacje na Liberapay + Błędne wyrażenie regularne! + Nie znaleziono żadnych osi czasu na tej instancji! + Usunąć tą instancję? + Przetłumacz na + Obserwuj instancję + Już obserwujesz tą instancję! + Instancja jest śledzona! + Partnerstwa + Informacje + Ukryj podbicia od %s + Polecaj na profilu + Pokazuj podbicia od %s + Nie polecaj na profilu + Konto jest teraz polecane na profilu + Konto nie jest już polecane na profilu + Podbicia są teraz pokazywane! + Podbicia są teraz ukrywane! + Bezpośrednia wiadomość + Filtry + Brak filtrów. Możesz dodać filtr używając przycisku \"+\". + Słowo kluczowe lub fraza + Lokalna oś czasu + Publiczne osi czasu + Powiadomienia + Rozmowy + Zostanie dopasowany, niezależnie od wielkości liter w tekście lub ostrzeżenia o treści toot-a + Pomiń zamiast ukrycia + Odfiltrowane toot-y znikną bezpowrotnie, nawet jeśli filtr zostanie zdjęty + Jeśli słowo kluczowe zawiera tylko tylko litery i cyfry, będzie stosowany tylko jeśli dopasuje cały wyraz + Cały wyraz + Filtruj konteksty + Jeden lub wiele kontekstów, gdzie stosuje się filtr + Wygasaj po + Usunąć filtr? + Aktualizuj filtr + Utwórz Filtr + Kogo obserwować + Brak kont na liście w tej chwili! + Obserwuj + Zaznacz wszystko + Odznacz wszystko + Konto %s jest już obrserwowane! + Tworzenie listy %s + Dodawanie kont do listy + Konta zostały dodane do listy + Dodawanie kont do listy + Nie ma jeszcze utworzonej żadnej listy. Kliknij \"+\" aby dodać nową. + Kogo obserwować + Trunk API + Nie ma możliwości obserwowania kont(a) + Pobieram zdalne konto + Automatycznie pokaż ukryte treści multimedialne + Nowy śledzący + Nowe Podbicie + Nowe ulubione + Nowe Wspomnienie + Głosowanie zakończone + Nowy Wpis + Kopia zapasowa wpisów + Nowe posty + Pobrane Media + Zmień dźwięk powiadomień + Wybierz Dźwięk + Włącz przedział czasowy + Filmy instruktażowe + Pobieram zdalny wątek! + Brak zablokowanych domen! + Odblokuj domenę + Czy na pewno chcesz odblokować %s? + Czy na pewno chcesz zablokować %s? + Zablokowane domeny + Zablokuj domenę + Domena jest zablokowana + Domena nie jest już zablokowana! + Pobieram zdalny status + Skomentuj + Instancja Peertube + Bądź pierwszą osobą, która doda komentarz przyciskiem po prawej na górze! + %s wyświetleń + Długość: %s + Dodaj instancję + Komentarze nie są włączone na tym filmie! + Wybierz rozdzielczość + Ulubione peertube + Dodano film do zakładek! + Film został usunięty z zakładek! + Nie ma filmów peertube w twoich ulubionych! + Kanał + Filmy + Kanały + Użyj Emoji One + Informacja + Wyświetlaj podgląd we wszystkich wpisach + Nowy projektant UX/UI + Wyświetl podglądy filmów + Id konta został skopiowany do schowka! + Zmień język + Domyślny język + Przycinaj długie tooty + Przycinaj tooty powyżej \'x\' linii. Zero oznacza wyłączenie. + Wyświetl więcej + Wyświetl mniej + Zarządzaj tagami + Tag już istnieje! + Tag został zapisany! + Tag został zmieniony! + Tag został usunięty! + Zaplanuj podbicie + Podbicie jest zaplanowane! + Brak zaplanowanych podbić! + Zaplanuj podbicie.]]> + Oś sztuki + Otwórz menu + Powrót + Logo aplikacji + Zdjęcie profilowe + Baner profilowy + Kontakt z administratorem instancji + Dodaj nowy + Logo MastoHost + Wybieracz emoji + Odśwież + Rozszerz rozmowę + Usuń konto + Usuń zablokowaną domenę + Własny wybieracz emoji + Odtwórz film + Nowy wpis + Obraz karty + Ukryj media + Favicon + Obraz przy dodawaniu opisu + + Nigdy + 30 minut + 1 godzina + 6 godzin + 12 godzin + 1 dzień + 1 tydzień + + W tym polu, musisz wpisać nazwę hosta instancji.\nNa przykład, jeżeli konto było zakładane na https://mastodon.social\npo prostu wpisz mastodon.social (bez https://)\n + Po wpisaniu kilku liter pojawią sie sugestie.\n\n + ⚠ Przycisk logowania zadziała tylko jeśli wpisana nazwa jest poprawna i a sama instancja działa! + Więcej informacji + + Języki + Tylko media + Pokaż NSFW + Tłumaczenia Crowdin + Menedżer Crowdin + Tłumaczenie aplikacji + O serwisie Crowdin + Bot + Instancja pixelfed + Instancja Mastodon + Którekolwiek z wymienionych + Wszystkie z wymienionych + Zadne z wymienionych + Dowolne z tych słów (oddzielone spacją) + Wszystkie z tych słów (rozdzielone spacją) + Dodaj kilka słów do filtru (oddzielone spacjami) + Zmień nazwę kolumny + Instancja Misskey + Brak zainstalowanej aplikacji do otwarcia tego odnośnika. + Subskrypcje + Przegląd + Popularne + Ostatnio dodane + Lokalne + Wysyłanie + Odpowiedz + Usuń komentarz + Czy na pewno chcesz usunąć ten komentarz? + Odtwarzanie pełnoekranowe + Tryb wideo + Wybierz plik do przesłania + Moje filmy + Tytuł + Licencja + Kategoria + Język + Ten film zawiera dojrzałe lub nieodpowiednie treści + Włącz komentarze na filmach + Aktualizuj filmy + Opis + Film został zaaktualizowany! + Wysyłanie anulowane! + Film został przesłany! + Przesyłanie, proszę czekać… + Naciśnij tutaj, aby edytować informacje o filmie. + Usuń film + Czy na pewno chcesz usunąć ten film? + Wyświetlaj filmy NSFW + Brak filmów do wyświetlenia! + Zostaw komentarz + Udostępnij + Wybierz tryb harmonogramu + Z urządzenia + Z serwera + Wpisy (Serwer) + Wpisy (Urządzenie) + Modyfikuj + Wyświetl nowe wpisy nad przyciskiem \"Pobierz wpisy\" + Osi czasu + Interfejs + Kontakty + %1$s skomentował twój film %2$s]]> + %1$s śledzi twój kanał %2$s]]> + %1$s śledzi twoje konto]]> + %1$s został opublikowany]]> + %1$s zakończony powodzeniem]]> + %1$s nie powiódł się]]> + %1$s opublikował nowy film: %2$s]]> + %1$s został dodany do czarnej listy]]> + %1$s został usunięty z czarnej listy]]> + Eksportuj dane + Importuj dane + Wybierz plik do zaimportowania + Wystąpił błąd podczas wybierania pliku z kopią zapasową! + Dodaj publiczny komentarz + Wyślij komentarz + Brak połączenia z Internetem. Twoja wiadomość została zapisana w szkicach. + Zwykły tekst + HTML + Markdown + Wyloguj konto + Wszystko + Wspomóż aplikację + Open Collective umożliwia grupom szybkie tworzenie kolektywów, pozyskiwanie funduszy i zarządzanie nimi w przejrzysty sposób. + Skopiuj adres + Połącz + Normalny + Kompaktowy + Konsola + Ustaw tryb wyświetlania + Patch the Security Provider + Aktualizuj śledzące domeny + Baza danych śledzących domen została zaktualizowana! + połączenia http zablokowane przez aplikację + Lista zablokowanych połączeń + Wyślij + Baza danych została wyeksportowana! + Wyróżnione hashtagi + Filtruj oś czasu z tagami + Brak tagów + Ukryj przycisk \"Usuń\" na karcie Powiadomienia + Dołącz obraz podczas udostępniania adresu URL + + Głosowanie + Głosowania + Utwórz głosowanie + Opcja 1 + Opcja 2 + Opcja %d + Potrzebujesz conajmniej dwóch opcji dla głosowania! + Gotowe + zakończ o %s + Odśwież głosowanie + Zagłosuj + Głosowanie, w którym brałeś(-aś) udział, zostało zakończone + Głosowanie, które stworzyłeś(-aś), zostało zakończone + Dostosuj + Kategorie + Przedział czasowy + Zaawansowane + Wyświetl plakietkę \"nowy\" na nieprzeczytanych wpisach + Peertube + Przesuń oś czasu + Ukryj oś czasu + Zmień kolejność osi czasu + Lista permanentnie usunięta + Śledzona instancja usunięta + Przypięty tag usunięty + Cofnij + Musisz mieć dwie widoczne karty! + Zmień kolejność osi czasu + Główne osi czasu mogą jedynie być ukryte! + BBCode + Zawsze oznaczaj media jako wrażliwe + Instancja GNU + Wpis zapisany w pamięci podręcznej + Przekaż tagi w odpowiedziach + Long press to store media + Rozmycie wrażliwych treści + Display timelines in a list + Display timelines + Oznacz konta botów w wpisach + Zarządzaj tagami + Remember the position in Home timeline + Historia + Playlisty + Wyświetlana nazwa + You don\'t have any playlists. Tap on the \"+\" icon to add a new playlist + You must provide a display name! + The channel is required when the playlist is public. + Utwórz playlistę + Jeszcze nic nie ma w tej playliście. + ponów + Galeria + Emoji + Naklejka + Wykasuj + Tekst + Filtr + Pędzel + Czy na pewno chcesz wyjść bez zapisywania obrazu? + Odrzuć + Zapisywanie… + Obraz zapisany pomyślnie! + Nie udało się zapisać obrazu + Przezroczystość + Włącz edytor zdjęć + Dodaj opcję + Usuń ostatnią opcję + Wycisz rozmowę + Wyłącz wyciszenie rozmowy + Rozmowa nie jest już wyciszona! + Ta rozmowa jest wyciszona + Open application features + Czasowe wyciszenie + Mention the account + Odśwież pamięć podręczną + Mention the status + Aktualności + Ogólne + Regionalne + Sztuka + Dziennikarstwo + Aktywizm + Gry + Technologia + Zawartość dla dorosłych + Futrzaki + Jedzenie + Logo instancji + Coś poszło nie tak podczas sprawdzania dostępnych instancji! + Dołącz do Mastodona + Choose an instance by picking up a category, then tap on a check button. + Choose an instance by tapping on a check button. + %1$s użytkowników + Potwierdź hasło + Zgadzam się z %1$s i %2$s + zasady serwera + warunki korzystania z usługi + Zarejestruj się + This instance works with invitations. Your account will need to be manually approved by an administrator before being usable. + Proszę wypełnić wszystkie pola! + Hasła nie pasują do siebie! + Wygląda na to, że adres e-mail nie jest prawidłowy! + Twoja nazwa użytkownika będzie unikalna na %1$s + Wyślemy Ci e-mail z potwierdzeniem + Użyj co najmniej 8 znaków + Hasło powinno zawierać co najmniej 8 znaków + Nazwa użytkownika powinna zawierać tylko litery, cyfry i podkreślenia + Konto zostało utworzone! + Your account has been created!\n\n + Think to validate your email within the 48 next hours.\n\n + You can now connect your account by writing %1$s in the first field and tap on Connect.\n\n + Important: If your instance required validation, you will receive an email once it is validated! + + Zapisać wiadomość jako szkic? + Administracja + Zgłoszenia + Brak zgłoszeń do wyświetlenia! + Połącz ponownie ze swoim kontem + The application failed to access the admin features. You might need to reconnect the account for having the correct scope. + Nierozwiązane + Zdalne + Aktywne + Oczekujące + Wyłączone + Wyciszone + Zawieszone + Uprawnienia + Stan e-mail + Status logowania + Dołączono + Najnowsze IP + Ostrzeż + Wyłącz + Wycisz + Notify the user per e-mail + Niestandardowe ostrzeżenie + Użytkownik + Moderator + Administrator + Potwierdzono + Nie potwierdzono + Zgłoszone statusy + Konto + Cofnij wyciszenie + Cofnij wyłączenie + Zawieś + Cofnij zawieszenie + Konto jest wyciszone! + Konto nie jest już wyciszone! + Konto jest zawieszone! + Konto nie jest już zawieszone! + Konto jest wyłączone! + Konto nie jest już wyłączone! + Konto zostało ostrzeżone! + Wyświetl menu administratora + Display the admin feature in statuses + Zezwól + Konto zostało zatwierdzone! + Konto zostało odrzucone! + Assign to me + Unassign + Mark as resolved + Mark as unresolved + Brak Zawartości! + Display Fedilab features button + The application needs to access audio recording + Wiadomość głosowa + Włącz szybką odpowiedź + The account you are replying might not see your message! + If disabled, the app will always load last statuses + If disabled, sensitive media will be hidden with a button + Store media in full size with a long press on previews + Add an ellipse button at the top right for listing all tags/instances/lists + During the time slot, the app will send notifications. You can reverse (ie: silent) this time slot with the right spinner. + Display a Fedilab button below profile picture. It is a shortcut for accessing in-app features. + Allow to reply directly in timelines below statuses + Previews will not be cropped in timelines + Allow to play embedded videos directly in timelines + Allow to reverse the way to read statuses that are displayed once tapping the fetch more button + This option allows to support recent cipher suites. It is useful for older Android devices or if you cannot connect to your instance. + Exclusively for Peertube videos. Switch this mode if you cannot play them. + These tags will allow to filter statuses from profiles. You will have to use the context menu for seeing them. + Automatically insert a line break after the mention to capitalize the first letter + Allow content creators to share statuses to their RSS feeds + Kompozycja + Maximum retry times when uploading media + Utwórz tutaj nowy folder + Enter the folder name + Wprowadź prawidłową nazwę folderu + Folder o takiej nazwie już istnieje.\n Proszę podać inną nazwę dla tego folderu + Wybierz + Domyślny folder + Folder + Utwórz folder + Display a toast message after an action has been completed (boost, fav, etc.)? + Muted instances have been exported! + Dodaj instancję + Eksportuj instancje + Importuj instancje + Raport o błędach + Włącz raportowanie błędów + Jeśli włączone, raport o awarii zostanie utworzony lokalnie, a następnie będziesz mógł go udostępnić. + Fedilab został zatrzymany :( + Możesz wysłać mi email z raportem o błędzie. To pomoże go naprawić :)\n\nMożesz dopisać coś więcej. Dziękuję! + Użyj wysiwyg + When enabled, you will be able to format your text easily with tools. + Statystyki + Całkowita liczba statusów + Liczba podbić + Liczba ulubionych + Liczba wzmianek + Number of follows + Liczba ankiet + Liczba odpowiedzi + Liczba statusów + Statusy + Widoczność + Number with media + Number with sensitive media + Number with CW + Data wysłania pierwszego statusu + Data wysłania ostatniego statusu + Data pierwszego powiadomienia + Data ostatniego powiadomienia + Częstotliwość + %s statuses per day + %s powiadomień dziennie + Przedział czasowy + Grupy + Brak grup! + Wyłącz niestandardowe animowane emotikony + Wykresy + Wyświetl wykresy + Aplikacja zbiera Twoje dane lokalne, proszę czekać… + Kopia zapasowa + Automatyczna kopia zapasowa statusów + This option is per account. It will launch a service that will automatically store your statuses locally in the database. That allows to get statistics and charts + Auto backup notifications + This option is per account. It will launch a service that will automatically store your notifications locally in the database. That allows to get statistics and charts + Zgłoś konto + Wyślij zaproszenie + Twoja instancja nie pozwala na zarejestrowanie nowego konta! + + %d głos + %d votes + %d votes + %d głosów + + + %d głosujący + %d voters + %d voters + %d głosujących + + + Pojedynczy wybór + Wielokrotny wybór + + + 5 minut + 30 minut + 1 godzina + 6 godzin + 1 dzień + 3 dni + 7 dni + + + Torrent + Widok sieci Web + + For joining my instance \"%1$s\", you can download Fedilab:\n\nF-Droid: %2$s\nGoogle: %3$s\n\nThen open the link below with Fedilab and create your account :)\n\n%4$s + + Twoja ankieta nie może mieć zduplikowanych opcji! + Dla wszystkich kont + Pamięć podręczna bazy danych + Clear your home timeline cache + Wyczyść statusy z pamięci podręcznej + Wyczyść zakładki + Pliki w pamięci podręcznej + Total notifications + Hide menu items + Fedilab is running live notifications + For %1$s accounts with %2$s events + Live notifications for %1$s + Live notifications will be enabled for this account. + Clear cache when leaving + The cache (media, cached messages, data from the built-in browser) will be automatically cleared when leaving the application. + Czy chcesz przestać obserwować to konto? + Show confirmation dialog before unfollowing + Zastąp linki do YouTube\'a linkami do Invidiousa + Invidious jest alternatywnym front-endem dla YouTube + Enter your custom host or leave blank for using invidious.snopyta.org + Przekieruj linki Twittera do Nitter + Nitter to alternatywna wersja front-end dla Twittera, która skupia się na prywatności. + Wprowadź swój niestandardowy host lub pozostaw puste, aby użyć nitter.net + Replace Instagram with Bibliogram + Bibliogram jest alternatywną wersją front-end na Instagramie typu open source, koncentrującą się na prywatności. + Enter your custom host or leave blank for using bibliogram.art + Replace Reddit with Libreddit + Libreddit to alternatywny, otwarty front-end dla Reddita, skupiający się na prywatności. + Enter your custom host or leave blank for using libredd.it + Replace Medium links + Replace medium.com links with an open source alternative front-end focused on privacy. + Default: scribe.rip + Replace Wikipedia links + Replace Wikipedia link with an open source alternative front-end focused on privacy. + Default: wikiless.org + Ukryj pasek powiadomień Fedilab + For hiding the remaining notification in the status bar, tap on the eye icon button then uncheck: \"Display in status bar\" + Use a push notifications system for getting notifications in real time. + No live notifications + Live notifications + Powiadomienia będą pobierane co 15 minut. + Dodaj notatki + Notatki dla tego konta + Allow to compress large photos into smaller sized photos with very less or negligible loss in quality of the image. + Allow compressing videos while maintaining their quality. + The app is compressing the media, it can be quite long… + Zmień ikonę aplikacji + Kliknij, aby zmienić ikonę aplikacji + Publikuj + Widoczność wpisu + Kliknij tutaj, aby dodać zdjęcia + Akceptowane formaty: jpeg, png, gif \n\nMaksymalny rozmiar pliku: 15 MB \n\nAlbumy mogą zawierać do 4 zdjęć lub filmów + Prześlij multimedia + Dodaj opcjonalny podpis + Aplikacja otrzymała bardzo długi komunikat o błędzie od API %1$s + Podgląd wiadomości + Dodaj wzmianki w każdej wiadomości + Pobieranie rozmowy + Sortuj według + Tytuł filmu + Dołącz do Peertube + I am at least 16 years old and agree to the %1$s of this instance + Linki + Zmień kolor linków (URL, wzmianki, tagi itp.) w wiadomościach + Reblogs header + Change the color of display name at the top of messages + Change the color of the user name at the top of messages + Change the color of the header for reblogs + Posty + Background color of posts in timelines + Resetuj kolory + Tap here to reset all your custom colors + Resetuj + Ikony + Color of bottom icons in timelines + Przypnij ten tag + Logo instancji + Edytuj profil + Make an action + Tłumaczenie + Podgląd obrazu + Kolor tekstu + Change the text color in pots + Zastosuj zmiany + You need to restart the application to apply changes + Uruchom ponownie + Użyj niestandardowego motywu + Allow to override colors of the selected theme above + Motyw + Store before + Motyw został wyeksportowany + Motyw został pomyślnie wyeksportowany do pliku CSV + Apply the primary color to the status bar + Kolor paska stanu + Przywróć domyślny motyw + Importuj motyw + Kliknij tutaj, aby zaimportować motyw z poprzedniego eksportu + Eksportuj motyw + Kliknij tutaj, aby wyeksportować bieżący motyw + An error occurred when selecting the theme file + Wybór motywu + Wybierz preinstalowany motyw + Motywy + Apply the primary color to the navigation bar + Navigation bar color + The underlying color of the app’s content. + Kolor tła + Accents select parts of the UI. + Kolor akcentu + Displayed most frequently across your app. + Kolor podstawowy + Export bookmarks to the instance + Import bookmarks from the instance + User count + Liczba statusów + Instance count + Blocked + Kończy się za %s + Co nowego w %s + Możesz obserwować moje konto w celu śledzenia aktualizacji + This instance is not available on https://instances.social + Wyświetl pełny link + Udostępnij link + Adres URL został skopiowany do schowka + Open with another app + Check redirect + This URL does not redirect + %1$s \n\nredirects to\n\n %2$s + Change the user agent + Set a custom user agent or leave blank + Allows to customize the user agent used for api calls or with the built-in browser. + Usuń parametry UTM + The app will automatically remove UTM parameters from URLs before visiting a link. + Trendy + Popularne teraz + %d people talking + Konta Twitter (przez Nitter) + Twitter usernames space separated + Identity proofs + Verified identity + Verified by %1$s (%2$s) + Delete the notification + Wyświetl więcej opcji + It is a Pixelfed story + Upload a media, it will be automatically added to your Pixelfed story. + Media successfully added to your story! + Action disabled + Przestań obserwować + Something went wrong, please check your download directory in settings. + Ogłoszenia + Brak ogłoszeń! + Dodaj reakcję + Use your favourite browser inside the app. Uncheck this feature to open links externally. + Video cache in MB, zero means no cache. + Znak wodny + Automatically add a watermark at the bottom of pictures. The text can be customized for each account. + No distributors found! + You need a distributor for receiving push notifications.\nYou will find more details at %1$s.\n\nYou can also disable push notifications in settings for ignoring that message. + Select a distributor + diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml new file mode 100644 index 000000000..9709137a9 --- /dev/null +++ b/app/src/main/res/values-pt/strings.xml @@ -0,0 +1,1142 @@ + + + Abrir menu + Fechar menu + Sobre + Sobre a instância + Privacidade + Cache + Sair + Entrar + + Fechar + Sim + Não + Cancelar + Baixar + Baixar %1$s + Mídia salva + Ficheiro: %1$s + Senha + E-mail + Contas + Toots + Tags + Salvar + Restaurar + Sem resultados! + Instância + Instância: mastodon.social + Usando a conta %1$s agora + Adicionar conta + O conteúdo do toot foi copiado para a área de transferência + O link do toot foi copiado para a área de transferência + Mudar + Selecione uma imagem… + Limpar + Câmera + Excluir tudo + Traduzir este toot. + Agendar + Tamanho dos textos e ícones + Alterar tamanho atual dos textos: + Alterar tamanho atual dos ícones: + Próximo + Anterior + Abrir com + Ok + Mídia + Compartilhar com + Compartilhado via Fedilab + Respostas + Nome de usuário + Rascunhos + Favoritos + Novos seguidores + Menções + Boosts + Mostrar boosts + Mostrar respostas + Abrir no navegador + Traduzir + Por favor, espere um pouco antes de fazer esta ação. + + Página inicial + Timeline local + Timeline global + Opções + Favoritos + Comunicação + Usuários silenciados + Usuários bloqueados + Notificações + Seguidores pendentes + Configurações + Remover conta + Remover a conta %1$s do aplicativo? + Mandar um e-mail + Toque no caminho para alterá-lo + Falhou! + Toots agendados + As informações abaixo podem refletir incompletamente o perfil do usuário. + Inserir emoji + O aplicativo não achou emojis personalizados no momento. + Notificações instantâneas + Tem a certeza que quer sair? + Tem a certeza que quer sair @%1$s@%2$s? + + Sem toots + Não existem histórias a exibir + Histórias + Levou boost de %1$s + Favoritar toot? + Desfavoritar toot? + Dar boost? + Desfazer boost? + Fixar este toot? + Desafixar este toot? + Silenciar + Bloquear + Denunciar + Excluir + Copiar + Compartilhar + Mencionar + Silenciar até... + Excluir & rascunhar + + Silenciar esta conta? + Bloquear esta conta? + Denunciar este toot? + Bloquear esta instância? + Desativar silêncio da conta? + Desbloquear esta conta? + + + Notificar + Silencioso + + + Excluir este toot? + Excluir & rascunhar este toot? + + Salvos + Salvar + Remover dos Salvos + Sem toots salvos + Toot foi salvo! + Toot foi removido dos Salvos! + + %d s + %d m + %d h + %d d + + %d segundo + %d segundos + + + %d minuto + %d minutos + + + %d hora + %d horas + + + %d dia + %d dias + + + Aviso de conteúdo + No que você está pensando? + TOOT! + QUEET! + ac + Compor toot + Responder toot + Compor queet + Responder queet + Selecionar mídia + Ocorreu um erro ao selecionar a mídia! + Remover mídia? + Toot vazio! + Visibilidade do toot + Visibilidade padrão dos toots: + Toot enviado! + Você está respondendo este toot: + Conteúdo sensível? + + Postar em timelines públicas + Não postar em timelines públicas + Postar apenas para seguidores + Postar apenas para usuários mencionados + + Sem rascunhos! + Escolha um toot + Escolha uma conta + Selecionar contas + Excluir rascunho? + Toque no botão para mostrar toot original + Descreva para os deficientes visuais + + Sem descrição! + + Versão %1$s + Desenvolvedor: + Licença: + GNU GPL V3 + Código-fonte: + Tradução dos toots: + Procure instâncias: + Designer do ícone: + + Conversa + + Sem conta + Sem seguidores pendentes + Toots \n %1$s + Seguindo \n %1$s + Seguidores \n %1$s + Fixado \n %d + Permitir + Recusar + + Sem toots agendados! + Componha um toot e toque em Agendar no topo do menu. + Excluir toot agendado? + Mídia: %d + Toot agendado! + A data de agendamento deve ser após o horário atual! + Economia de bateria ativada! Pode não funcionar como esperado. + + O tempo de silêncio deve ser maior do que um minuto. + %1$s foi silenciado até %2$s. \n Você pode desativar no perfil do usuário. + %1$s está silenciado até %2$s. \n Toque aqui para desativar o silêncio. + + Sem notificações + te mencionou + escreveu uma mensagem nova + deu boost no seu toot + favoritou seu toot + te seguiu + pediu para o seguir + + e outra notificação + e outras %d notificações + + + %d favoritou + %d favoritaram + + Excluir notificação? + Excluir todas as notificações? + Notificação excluída! + Todas as notificações foram excluídas! + + Seguindo + Seguidores + Fixado + + Não foi possível detectar id do cliente! + Não é possível ligar ao domínio de instância! + Sem conexão com a internet! + Conta bloqueada! + Conta desbloqueada! + Silêncio ativado! + Silêncio desativado! + Você seguiu a conta! + Você deixou de seguir a conta! + Toot compartilhado! + Boost desfeito! + Toot favoritado! + Toot desfavoritado! + Toot denunciado! + Toot excluído! + Toot fixado! + Toot desafixado! + Oops! Ocorreu um erro! + Ocorreu um erro! A instância não retornou um código de autorização! + Parece que o domínio da instância não é válido! + Ocorreu um erro ao alternar entre as contas! + Ocorreu um erro na pesquisa! + Dados de perfil salvos! + A ação não pode ser feita ou não é suportada + Mídia salva! + Ocorreu um erro na tradução! + Tradução está desativada nas configurações + Rascunho salvo! + Você tem certeza de que sua instância permite esse número de caracteres? Geralmente, são 500 caracteres. + Visibilidade dos toots foi alterada na conta %1$s + + Número de toots por vez + Sempre + Wi-Fi + Perguntar + Carregar mídia + Carregar imagens + Mostrar mais… + Mostrar menos… + Conteúdo sensível + Desativar GIF + Caminho: + Salvar rascunhos automaticamente + Adicionar link da mídia nos toots + Notificar quando alguém te seguir + Notificar quando alguém der boost nos seus toots + Notificar quando alguém favoritar seus toots + Notificar quando alguém te mencionar + Notificar quando uma sondagem terminar + Notificar para novas mensagens + Mostrar diálogo antes de dar boost + Mostrar diálogo antes de favoritar + Notificar somente em Wi-Fi + Notificar? + Silenciar notificações + Segundos para expirar a prévia de mídia sensível, 0 para desativar. + Tempo limite da descrição de ficheiros multimédia (segundos, 0 significa desligado) + Editar perfil + Compartilhamento externo personalizado + Seu link de compartilhamento externo… + Bio… + Trancar conta + Salvar alterações + Escolha uma imagem de cabeçalho + Ajustar prévias de imagens + Transformar toot em sequência ao exceder o valor definido: + Você excedeu os 160 caracteres permitidos! + Você excedeu os 30 caracteres permitidos! + Entre + e + O tempo deve ser maior que %1$s + O tempo deve ser menor que %1$s + Horário inicial + Horário final + Usar navegador interno + Abas personalizadas + Ativar Javascript + Expandir AC automaticamente + Permitir cookies de terceiros + Você pode deixar sua chave de API em branco para Yandex + + Noturno + Diurno + AMOLED + + Cor do LED: + + Azul + Ciano + Magenta + Verde + Vermelho + Amarelo + Branco + + Seguir + Desbloquear + Silenciar + Desativar silêncio + Solicitação enviada + Segue você + Pesquisar + Primeira letra em maiúscula para respostas + Redimensionar fotos + Redimensionar vídeos + + Notificações + Por favor, confirme que notificações você quer receber. + Você pode ativar ou desativar essas notificações mais tarde em configurações (na aba Notificações). + + + Limpar cache + Há %1$s de dados em cache.\n\nGostaria de limpá-lo? + Mb + Cache limpo! %1$s foram liberados + + Título + Título… + Descrição + Palavras-chave + Palavras-chave… + + Sincronizar + Filtro + Seus toots + Notificações + Público + Não-listado + Privado + Direto + Algumas palavras-chave… + Mostrar mídia + Mostrar fixado + Nenhum resultado correspondente encontrado! + Fazer backup dos toots de %1$s + %1$s novos toots foram importados + %1$s notificações foram importadas + + Data descendente + Data crescente + + + Não + Somente + Ambos + + Nenhum toot foi encontrado no banco de dados. Por favor, use o botão de sincronização no menu para recuperá-los. + + Dados salvos + Apenas informação básica das contas é armazenada no aparelho. + Estes dados são estritamente confidenciais e só podem ser usados pelo aplicativo. + Desinstalar o aplicativo imediatamente remove esses dados.\n + ⚠ Credenciais nunca são armazenadas. Elas só são usadas durante a autenticação segura (SSL) com uma instância. + + Permissões: + - ACCESS_NETWORK_STATE: Usada para detectar se o aparelho está conectado a uma rede Wi-Fi.\n + - INTERNET: Usado para consultar instâncias.\n + - WRITE_EXTERNAL_STORAGE: Usada para armazenar mídia ou mover o aplicativo para o cartão SD.\n + - READ_EXTERNAL_STORAGE: Usada para adicionar mídia aos toots.\n + - BOOT_COMPLETED: Usada para iniciar o serviço de notificação.\n + - WAKE_LOCK: Usada durante o serviço de notificação. + + Permissões da API: + - Leitura: Ler dados.\n + - Escrita: Publicar toots e enviar mídia nos toots.\n + - Seguir: Seguir, deixar de seguir, bloquear e desbloquear.\n\n + ⚠ Essas ações são realizadas apenas quando o usuário solicitá-las. + + Rastreio e Bibliotecas + O aplicativo não usa ferramentas de rastreio (medição de audiência, relatórios de erro, etc) e não contém qualquer propaganda.\n\n + O uso de bibliotecas é minimizado: \n + - Glide: Para gerenciar mídia\n + - Android-Job: Para gerenciar serviços\n + - PhotoView: Para gerenciar imagens\n + + Tradução de toots + O aplicativo oferece a habilidade de traduzir toots usando o idioma do aparelho e a API do Yandex.\n + Yandex tem a sua própria política de privacidade, que pode ser lida aqui: https://yandex.ru/legal/confidential/?lang=en + + Agradecimentos aos: + + Filtrar por expressões regulares + Pesquisa + Remover + Ver mais toots… + + Listas + Tem certeza de que deseja excluir permanentemente esta lista? + Não há nada nesta lista ainda. Quando membros desta lista postarem novos toots, eles aparecerão aqui. + Adicionar à lista + Criar lista + Excluir lista + Editar lista + Nome da nova lista + A conta foi adicionada à lista! + Ainda não tem uma lista! + + %1$s mudou-se para %2$s + Falha ao autenticar? + Aqui estão algumas verificações que podem ajudar:\n\n + - Verifique se não há erros de digitação no nome da instância\n\n + - Verifique se a sua instância não está fora do ar\n\n + - Se você utiliza a autenticação de dois fatores (2FA), use o link na parte inferior (uma vez que o nome da instância estiver preenchido) \n\n + - Você também pode usar este link sem usar o 2FA\n\n + - Se ainda não funcionar, por favor, crie um issue no Framagit em https://framagit.org/tom79/fedilab/issues + + A mídia foi carregada. Toque aqui para vê-la. + Esta ação pode ser demorada. Você será notificado quando ela terminar. + Ainda em execução, por favor, aguarde… + Exportar toots + Exportar toots para %1$s + %1$s toots de %2$s foram exportados. + Algo deu errado enquanto exportava dados de %1$s + Algo correu mal ao exportar os dados! + Algo correu mal ao importar os dados! + + Proxy + Ativar proxy? + Servidor + Porta + Login + Senha + Adicionar detalhes do toot ao compartilhar + Apoie o aplicativo no Liberapay + Há um erro na expressão regular! + Nenhuma timeline foi encontrada nesta instância! + Deixar de seguir esta instância? + Traduzir em %s + Seguir instância + Você já segue esta instância! + Você seguiu a instância! + Parcerias + Informação + Ocultar boosts de %s + Destacar no perfil + Mostrar boosts de %s + Não destacar no perfil + A conta agora é destacada no perfil + A conta não é mais destacada no perfil + Boosts agora são mostrados! + Boosts agora são ocultados! + Mensagem direta + Filtros + Sem filtros. Você pode criar um tocando no botão \"+\". + Palavra-chave ou frase + Página Inicial + Timelines públicas + Notificações + Conversas + Serão correspondidas independente de maiúsculas ou minúsculas no texto ou no aviso de conteúdo de um toot + Apagar em vez de ocultar + Os toots filtrados desaparecerão irreversivelmente, mesmo se o filtro for removido mais tarde + Quando a palavra-chave ou frase só tem alfanuméricos, ela será aplicada somente se combinar com a palavra inteira + Palavra inteira + Contextos do filtro + Um ou mais contextos onde o filtro deve ser aplicado + Expira após + Excluir filtro? + Atualizar filtro + Criar filtro + Quem seguir + Não há contas listadas no momento! + Seguir + Selecionar tudo + Desmarcar tudo + Você segue %s! + Criando a lista %s + Adicionar contas à lista + Contas adicionadas à lista + Adicionando contas à lista + Sem listas. Você pode criar uma tocando no botão \"+\". + Quem seguir: + Trunk API + Desculpe, é impossível seguir + Carregando conta remota! + Expandir automaticamente mídias ocultas + Novo seguidor + Novo Boost + Novo favorito + Nova menção + Sondagem terminada + Novo toot + Backup de Toots + Novas publicações + Baixar mídia + Alterar som de notificação + Selecionar toque + Ativar definição de momento + Tutoriais em vídeo + Carregando sequência remota! + Sem instâncias bloqueadas! + Desbloquear instância + Tem certeza de que quer desbloquear %s? + Tem certeza de que quer bloquear %s?\n\nSeus seguidores desta instância serão removidos, e você não verá nenhum conteúdo ou notificação desta instância. + Instâncias bloqueadas + Bloquear instância + Instância bloqueada! + Instância desbloqueada! + Carregando toot remoto! + Comentar + Instância Peertube + Seja o primeiro a comentar o vídeo tocando no botão superior direito! + %s visualizações + Duração: %s + Adicionar instância + Os comentários deste vídeo foram desativados! + Selecione uma resolução + Favoritos Peertube + Vídeo favoritado! + Vídeo desfavoritado! + Não há vídeos do Peertube em seus favoritos! + Canal + Vídeos + Canais + Usar Emoji One + Informação + Mostrar pré-visualizações em todos os toots + Designer da nova UX/UI + Mostrar prévias de vídeo + O nome de utilizador foi copiado para a área de transferência! + Mudar o idioma + Idioma padrão + Cortar toots longos + Limitar toots por \'x\' linhas. Zero significa desativar. + Mostrar mais + Mostrar menos + Gerenciar tags + A tag já existe! + A tag foi guardada! + A tag foi alterada! + A tag foi excluída! + Agendar boost + Boost agendado! + Sem boosts agendados! + Agendar boost.]]> + Cronologia Arte + Abrir menu + Retroceder + Logótipo do aplicativo + Foto do perfil + Banner do perfil + Contacte o administrador da instância + Adicionar novo/a + Logótipo do MastoHost + Seletor de emoji + Atualizar + Expandir a conversa + Sair de uma conta + Remover o domínio bloqueado + Seletor de emojis personalizado + Reproduzir vídeo + Novo toot + Imagem do cartão + Ocultar mídia + Favicon + Adicionar descrição da média (para os deficientes visuais) + + Nunca + 30 minutos + 1 hora + 6 horas + 12 horas + 1 dia + 1 semana + + Neste campo, você precisa digitar o nome da sua instância.\nPor exemplo, se você criou sua conta em https://mastodon.social\nApenas digite mastodon.social (sem https://)\n + Você pode começar digitando as primeiras letras e nomes serão sugeridos.\n\n + ⚠ O botão de login só funcionará se o nome da instância for válido e a instância estiver ativa! + + Mais informação + + Idiomas + Apenas mídia + Mostrar mídia sensível + Traduções no Crowdin + Administrador do Crowdin + Tradução do aplicativo + Sobre o Crowdin + Robô + Instância Pixelfed + Instância Mastodon + Qualquer um + Todos + Nenhum + Qualquer uma destas palavras (separadas por espaço) + Todas estas palavras (separadas por espaço) + Adicione palavras para filtrar (separadas por espaço) + Alterar nome da aba + Instância Misskey + Nenhum aplicativo instalado suporta este link. + Inscrições + Visão geral + Destaques + Recentemente adicionado + Local + Enviar + Responder + Excluir comentário + Tem certeza de que deseja excluir este comentário? + Vídeo em tela cheia + Modo para vídeos + Selecione o ficheiro a enviar + Meus vídeos + Título + Licença + Categoria + Idioma + Este vídeo possui conteúdo adulto ou explícito + Ativar comentários do vídeo + Atualizar vídeo + Descrição + O vídeo foi atualizado! + Envio cancelado! + O vídeo foi enviado! + Enviando, por favor aguarde… + Toque aqui para editar os dados do vídeo. + Excluir vídeo + Tem certeza de que deseja excluir este vídeo? + Mostrar vídeos sensíveis + Sem vídeos! + Deixe um comentário! + Compartilhar + Escolher como agendar + Do dispositivo + Do servidor + Toots (Servidor) + Toots (Disp) + Modificar + Mostrar novos toots acima do botão \"Ver mais\" + Timelines + Interface + Contatos + %1$s comentou seu vídeo %2$s]]> + %1$s está seguindo seu canal %2$s]]> + %1$s está seguindo sua conta]]> + %1$s foi publicado]]> + %1$s foi importado com sucesso]]> + %1$s]]> + %1$s publicou um novo vídeo: %2$s]]> + %1$s foi bloqueado]]> + %1$s foi desbloqueado]]> + Exportar dados + Importar dados + Selecione o ficheiro a importar + Ocorreu um erro ao selecionar o ficheiro de backup! + Adicionar um comentário público + Enviar comentário + Não há conexão com a internet. Sua mensagem foi salva em Rascunhos. + Texto simples + HTML + Markdown + Sair da conta + Tudo + Apoie o aplicativo + Open Collective permite aos grupos criar rapidamente um coletivo, angariar fundos e gerí-los de forma transparente. + Copiar link + Conectar + Normal + Compacto + Terminal + Definir modo de exibição + Corrigir o Provedor de Segurança + Atualizar domínios de rastreio + O banco de dados de rastreio foi atualizado! + Servidores bloqueados pelo aplicativo + Lista de servidores bloqueados + Enviar + O banco de dados foi exportado! + Hashtags em destaque + Filtrar timeline com tags + Sem tags + Ocultar o botão de excluir notificação na aba das notificações + Anexar uma imagem ao compartilhar um link + + Enquete + Enquetes + Criar uma enquete + Opção 1 + Opção 2 + Opção %d + Você precisa de pelo menos duas opções para a enquete! + Ok + termina em %s + Atualizar + Votar + Uma enquete em que você votou terminou + Uma enquete sua terminou + Personalizar + Categorias + Definição de momento para notificações + Avançado + Mostrar etiqueta \'new\' em toots não lidos + Peertube + Mover timeline + Ocultar timeline + Reordenar timelines + Lista permanentemente excluída + Instância seguida removida + Tag fixada removida + Desfazer + Você precisa manter duas abas visíveis! + Reordenar timelines + Timelines principais só podem ser ocultadas! + BBCode + Sempre marcar mídia como sensível + Instância GNU + Toots em cache + Repetir tags nas respostas + Toque longo para salvar mídia + Blur na mídia sensível + Mostrar timelines em uma lista + Mostrar timelines + Marcar robôs em toots + Gerenciar tags + Lembrar a posição na página inicial + Histórico + Listas de reprodução + Nome de exibição + Sem listas de reprodução. Você pode criar uma tocando no botão \"+\". + Você deve inserir um nome de exibição! + O canal é necessário quando a lista é pública. + Criar uma lista + Lista vazia. + refazer + Galeria + Emoji + Figurinha + Borracha + Texto + Filtro + Pincel + Você tem certeza de que deseja sair sem salvar? + Cancelar + Salvando… + Imagem salva! + Falha ao salvar imagem. + Transparência + Ativar editor de fotos + Adicionar um item + Excluir último item + Silenciar conversa + Desativar silêncio + Silêncio desativado! + Silêncio ativado! + Abrir recursos do aplicativo + Silêncio temporário + Mencionar conta + Atualizar cache + Mencionar toot + Notícias + Geral + Regional + Arte + Jornalismo + Ativismo + Jogos + Tecnologia + Conteúdo adulto + Furry + Comida + Logo da instância + Erro ao verificar instâncias disponíveis! + Entre no Mastodon + Escolha uma instância através das categorias e toque no botão para continuar. + Escolha uma instância tocando num botão de seleção. + %1$s usuários + Confirmar senha + Eu concordo com %1$s e %2$s + regras da instância + termos de serviço + Criar conta + Esta instância funciona por convites. Sua conta precisa ser aprovada manualmente por um administrador antes de ser usável. + Por favor, preencha tudo! + Senhas não combinam! + E-mail não parece válido! + Seu nome de usuário será único em %1$s + Você receberá um e-mail de confirmação + Usar no mínimo 8 caracteres + Senha deve conter no mínimo 8 caracteres + Nome de usuário deve conter somente letras, números e underlines + Conta criada! + Sua conta foi criada!\n\n + Pode ser necessário confirmar seu e-mail dentro de 48 horas.\n\n + Você pode conectar a sua conta digitando %1$s no primeiro campo e tocar em Entrar.\n\n + Atenção: Se a instância escolhida requer confirmação, você receberá um e-mail para confirmar a conta criada! + + Salvar toot como rascunho? + Administração + Relatórios + Nenhum relatório para exibir! + Restabelecer a conta + O aplicativo falhou ao aceder aos recursos da administraçāo. Você pode precisar de reconectar a conta por ter o âmbito de recursos correto. + Não Resolvido + Remoto + Ativa + Pendente + Desativado + Silenciado + Suspenso + Permissões + Estado do e-mail + Estado da sessão + Entrou + IP mais recente + Avisar + Desativar + Silenciar + Notificar o utilizador por e-mail + Aviso personalizado + Utilizador + Moderador + Administrador + Confirmado + Não confirmado + Estados relatados + Conta + Desfazer silenciar + Anular desativar + Suspender + Anular suspender + A conta está silenciada! + A conta já não está silenciada! + A conta está suspensa! + A conta já não está suspensa! + A conta está desativada! + A conta já não está desativada! + A conta foi avisada! + Exibir o menu administrativo + Exibir o recurso de administrador em estados + Autorizar + A conta está aprovada! + A conta foi rejeitada! + Atribuir-me a mim + Não atribuir + Marcar como resolvido + Marcar como não resolvido + Conteúdo vazio! + Exibir o botão de recursos Fedilab + O aplicativo precisa de aceder à gravação de áudio + Mensagem de voz + Ativar resposta rápida + A conta a que está a responder pode não ver sua mensagem! + Se desativado, o aplicativo carregará sempre os últimos estados + Se desativado, a média sensível será ocultada com um botão + Armazenar a média em tamanho completo com uma pressão longa sobre as pré-visualizações + Adicionar um botão elipse na parte superior direita para listar todas as tags/instâncias/listas + Durante o período de tempo, o aplicativo enviará notificações. Você pode reverter (i. e.: silencioso) este período de tempo com o botão da direita. + Exibir um botão Fedilab abaixo da imagem do perfil. É um atalho para aceder a recursos dentro do aplicativo. + Permitir responder diretamente em cronologias abaixo dos estados + Pré-visualizações não serão cortadas em cronologias + Permitir a reprodução de vídeos incorporados diretamente em cronologias + Permite reverter o caminho para ler os estados que são exibidos quando clicar no botão buscar mais + Esta opção permite suportar conjuntos de codificação recentes. Ela é útil para dispositivos Android mais antigos ou se você não consegue ligar-se à sua instância. + Exclusivamente para vídeos do Peertube. Mude este modo se você não conseguir reproduzi-los. + Estas tags permitirão filtrar estados de perfis. Você terá que usar o menu de contexto para vê-los. + Inserir automaticamente uma quebra de linha, após a menção, para colocar a primeira letra em maiúscula + Permitir que criadores de conteúdo partilhem estados para os seus feeds RSS + Compor + Máximo de tentativas quando fizer o upload de média + Criar uma nova pasta aqui + Digite o nome da pasta + Por favor, digite um nome de pasta válido + Esta pasta já existe.\n Por favor, dê outro nome à pasta + Selecionar + Diretório inicial + Pasta + Criar pasta + Exibir uma mensagem de felicitação após uma ação ser concluída (partilhar, favorizar, etc.)? + As instâncias silenciadas foram exportadas! + Adicionar instância + Exportar instâncias + Importar instâncias + Relatórios de erros + Ativar relatórios de erros + Se ativado, um relatório de erro será criado localmente e poderá partilhá-lo. + Fedilab parou :( + Você pode me enviar por e-mail o relatório de erros. Irá me ajudar a corrigí-los :) \n\nVocê pode adicionar conteúdo adicional. Obrigado! + Usar o wysiwyg + Quando ativado, será capaz de formatar facilmente o seu texto com ferramentas. + Estatísticas + Todos os estados + Número de boosts + Número de favoritos + Número de menções + Número de seguidores + Número de votações + Número de respostas + Número de estados + Estados + Visibilidade + Número de ficheiros multimédia + Número com média sensível + Número com CW + Data do primeiro estado + Última data de estado + Primeira data de notificação + Última data de notificação + Frequência + %s estados por dia + %s notificações por dia + Intervalo de datas + Grupos + Sem grupos! + Desactivar emojis animados personalizados + Gráficos + Exibir gráficos + A aplicação está a recolher os seus dados locais, por favor aguarde… + Cópia de segurança + Estado da cópia de segurança + Esta opção é por conta. Vai lançar um serviço que grava automaticamente na base de dados local os seus estados. Esse serviço permite receber estatísticas e gráficos + Notificações da cópia de segurança automática + Esta opção é por conta. Lança um serviço que grava automaticamente na base de dados local os seus estados. Esse serviço permite receber estatísticas e gráficos + Denunciar conta + Enviar convite + A sua instância não permite registar uma conta nova! + + %d voto + %d votos + + + %d votante + %d votantes + + + Uma opção + Múltiplas opções + + + 5 minutos + 30 minutos + 1 hora + 6 horas + 1 dia + 3 dias + 7 dias + + + Webview + Transmissão direta + + Para aderir à minha instância \"%1$s\", pode transferir o Fedilab:\n\nF-Droid: %2$s\nGoogle: %3$s\n\nEm seguida, abra o link abaixo com o Fedilab e crie a sua conta :)\n\n%4$s + + A sua votação não pode ter opções duplicadas! + Para todas as contas + Cache da base de dados + Limpar o cache da timeline da sua página inicial + Limpar os seus estados em cache + Limpar marcadores + Ficheiros em cache + Todas as notificações + Ocultar itens do menu + Fedilab está a executar notificações instantâneas + Para %1$s contas com %2$s eventos + Notificações instantâneas para %1$s + As notificações instantâneas serão ativadas para esta conta. + Limpar cache ao sair + The cache (media, cached messages, data from the built-in browser) will be automatically cleared when leaving the application. + Quer deixar de seguir esta conta? + Mostrar diálogo de confirmação antes de deixar de seguir uma conta + Substituir Youtube pelo Invidio.us + Invidious é um interface, front-end, alternativo ao Youtube + Insira o seu host personalizado ou deixe em branco para utilizar o invidious.snopyta.org + Substituir o Twitter pelo Nitter + Nitter é um interface, front-end, alternativo ao Twitter de código aberto e focado na privacidade. + Insira o seu host personalizado ou deixe em branco para utilizar o nitter.net + Substituir o Instagram pelo Bibliogram + Bibliogram é um interface, front-end, alternativo ao Instagram de código aberto e focado na privacidade. + Insira o seu host personalizado ou deixe em branco para utilizar o bibliogram.art + Substituir o Reddit pelo Libreddit + Libreddit é um interface, front-end, alternativo ao Reddit de código aberto e focado na privacidade. + Insira o seu host personalizado ou deixe em branco para utilizar o libredd.it + Replace Medium links + Replace medium.com links with an open source alternative front-end focused on privacy. + Default: scribe.rip + Replace Wikipedia links + Replace Wikipedia link with an open source alternative front-end focused on privacy. + Default: wikiless.org + Ocultar a barra de notificações do Fedilab + Para ocultar as notificações restantes na barra de status, toque no botão com o ícone do olho e desmarcar: \"Mostrar na barra de status\" + Use um sistema notificações instantâneas para ter notificações em tempo real. + Sem notificações + Notificações ao vivo + As notificações serão obtidas a cada 15 minutos. + Adicionar notas + Notas da conta + Permitir compactar imagens grandes em imagens de tamanho menor com percas pequenas ou negligenciáveis de qualidade. + Permitir a compressão de vídeos mantendo a sua qualidade. + A aplicação está a comprimir os ficheiros, pode demorar algum tempo… + Alterar o ícone da aplicação + Toque para alterar o ícone da aplicação + Publicar + Visibilidade da mensagem + Toque para adicionar fotografias + Formatos aceites: jpeg, png, gif \n\nTamanho máximo: 15 MB \n\nOs álbuns poder conter até 4 fotografias ou vídeos + Enviar ficheiros + Adicionar uma descrição opcional + A aplicação recebeu da API uma mensagem de erro muito longa %1$s + Pré-visualizar mensagem + Adicionar menções em cada mensagem + A transferir conversa + Ordenar por + Titulo para o vídeo + Junte-se ao Peertube + Tenho pelo menos 16 anos de idade e concordo com os %1$s desta instância + Links + Alterar a cor dos links (URLs, menções, tags, etc.) nas mensagens + Cabeçalho de Reblogs + Alterar a cor do nome de exibição no topo das mensagens + Alterar a cor do nome do utilizador no topo das mensagens + Alterar a cor do cabeçalho para reblogs + Publicações + Cor de fundo das publicações nas cronologias + Repor cores + Toque aqui para repor todas as suas cores personalizadas + Reset + Icons + Color of bottom icons in timelines + Pin this tag + Logo of the instance + Edit profile + Make an action + Translation + Image preview + Text color + Change the text color in pots + Apply changes + You need to restart the application to apply changes + Restart + Use a custom theme + Allow to override colors of the selected theme above + Theming + Store before + The theme was exported + The theme has been successfully exported in CSV + Apply the primary color to the status bar + Status bar color + Restore a default theme + Import a theme + Tap here to import a theme from a previous export + Export the theme + Tap here to export the current theme + An error occurred when selecting the theme file + Theme Picker + Select a pre-installed theme + Themes + Apply the primary color to the navigation bar + Navigation bar color + The underlying color of the app’s content. + Background color + Accents select parts of the UI. + Accent color + Displayed most frequently across your app. + Primary color + Export bookmarks to the instance + Import bookmarks from the instance + User count + Status count + Instance count + Blocked + End in %s + What\'s new in %s + You can follow my account for updates + This instance is not available on https://instances.social + Exibir link completo + Partilhar link + O link foi copiado para a área de transferência + Abrir noutra aplicação + Check redirect + This URL does not redirect + %1$s \n\nredirects to\n\n %2$s + Change the user agent + Set a custom user agent or leave blank + Allows to customize the user agent used for api calls or with the built-in browser. + Remove UTM parameters + The app will automatically remove UTM parameters from URLs before visiting a link. + Trends + Trending now + %d people talking + Twitter accounts (via Nitter) + Twitter usernames space separated + Identity proofs + Verified identity + Verified by %1$s (%2$s) + Delete the notification + Display more options + It is a Pixelfed story + Upload a media, it will be automatically added to your Pixelfed story. + Media successfully added to your story! + Action disabled + Unfollow + Something went wrong, please check your download directory in settings. + Announcements + No announcements! + Add a reaction + Use your favourite browser inside the app. Uncheck this feature to open links externally. + Video cache in MB, zero means no cache. + Watermarks + Automatically add a watermark at the bottom of pictures. The text can be customized for each account. + No distributors found! + Necessita de um distribuidor para receber notificações instantâneas. \nVai encontrar mais detalhes em %1$s.\n\nPode desactivar as mensagens instantâneas nas configurações para ignorar esta mensagem. + Select a distributor + diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml new file mode 100644 index 000000000..e581f0188 --- /dev/null +++ b/app/src/main/res/values-ro/strings.xml @@ -0,0 +1,1148 @@ + + + Deschide meniul + Închide meniul + Despre + Despre instanță + Confidențialitate + Memorie cache + Deconectare + Autentificare + + Închide + Da + Nu + Anulare + Descărcați + Descărcat %1$s + Media salvate + Fișier: %1$s + Parolă + Email + Conturi + Notițe + Etichete + Salvează + Restabilire + Nici un rezultat! + Instanță + Instanță: mastodon.social + Acum lucrează cu contul %1$s + Adaugă un cont + Conținutul notiței a fost copiat în planșetă + The URL of the toot has been copied to the clipboard + Modifică + Selectează o imagine… + Elimină + Camera + Șterge tot + Traduceți această notiță. + Planificare + Dimensiunea textului și a iconiței + Modificaţi dimensiunea textului: + Modificați dimensiunea iconiței: + Următor + Anterior + Deschide cu + Validează + Media + Distribuie cu + Distribuie via Fedilab + Răspunsuri + Utilizator + Ciorne + Favorite + Noi persoane care mă urmăresc + Mențiuni + Amplifică + Arată amplificările + Arată răspunsurile + Deschide în browser + Traducere + Vă rugăm așteptați câteva secunde înainte de a efectua această acțiune. + + Prima pagină + Cronologie locală + Cronologie federată + Opțiuni + Favorite + Comunicare + Utilizatori trecuți la modul silențios + Utilizatori blocați + Notificări + Urmărește cererea + Setări + Șterge contul + Șterge contul %1$s din aplicație? + Trimite un email + Faceţi clic pe cale pentru a o schimba + Nereușit! + Notițe programate + Informaţiile de mai jos pot reflecta profilul incomplet al utilizatorului. + Inerare emoji + Pentru moment, aplicația nu a colectat emojis personalizate. + Push notifications + Are you sure you want to logout? + Are you sure you want to logout @%1$s@%2$s? + + Nu sunt notițe de afișat + No stories to display + Stories + Amplificat de %1$s + Adauga acestă notiță la favorite? + Înlaătură acestă notiță de la favorite? + Amplifică aceată notiță? + Oprește amplificarea notiței? + Fixează această notiță? + Anulează fixarea acestei notițe? + Silenţios + Blochează + Raportează + Elimină + Copiază + Distribuie + Menționează + Timed mute + Delete & re-draft + + Dezactivează acest cont? + Blochează acest cont? + Raportează acestă notiță? + Block this domain? + Unmute this account? + Unblock this account? + + + Notify + Silent + + + Eliminați această notiță? + Delete & re-draft this toot? + + Bookmarks + Add to bookmarks + Remove bookmark + No bookmarks to display + Status has been added to bookmarks! + Status was removed from bookmarks! + + %d s + %d m + %d h + %d d + + %d second + %d seconds + %d seconds + + + %d minute + %d minutes + %d minutes + + + %d hour + %d hours + %d hours + + + %d day + %d days + %d days + + + Atenționare + La ce te gândești? + Notiță! + QUEET! + cw + Scrie o notiță + Răspunde la această notiță + Write a queet + Reply to a queet + Selectați media + A aparut o eroare în timp ce selectați media! + Ștergeți? + Notița este goală! + Vizibilitatea notiței + Vizibilitatea implicită a notițelor: + Notița a fost trimisă! + Răspundeți la această notiță: + Conţinut sensibil? + + Postează pe coronologii publice + Nu posta pe cronologii publice + Postează doar pentru cei care te urmăresc + Postează la utilizatorii menționați + + Nu sunt ciorne! + Alege o notiță + Alege un cont + Selectați câteva conturi + Eliminați ciorna? + Apăsați butonul pentru a afișa notița originală + Descrie pentru nevăzători + + Nici o descriere disponibilă! + + Lansare %1$s + Programator: + Licență: + GNU GPL V3 + Sursa codului: + Traducerea notițelor: + Căutare instanțe: + Designer-ul iconiței: + + Conversație + + Nu există conturi pentru a fi afișate + Nici o cerere de urmărire + Notițe \n %1$s + Urmăriți \n %1$s + Cei care vă urmăresc \n %1$s + Fixat \n %d + Permite + Respinge + + Nici o notiță programtă pentru a afișa! + Scrie o notiță și apoi alege Planifică din meniul de sus. + Șterge notița planificată? + Media: %d + Notița a fost planificată! + Data planificării trebuie să fie mai mare decât ora curentă! + Economizor de baterie este activat! Acesta ar putea să nu funcţioneze conform aşteptărilor. + + The time for muting should be greater than one minute. + %1$s has been muted until %2$s.\n You can unmute this account from their profile page. + %1$s is muted until %2$s.\n Tap here to unmute the account. + + Nici o notificare de afişat + ai fost menționat + wrote a new message + a amplificat statusul tău + a favorizat statusul tău + vă urmărește + asked to follow you + + și încă o notificare + și incă %d alte notificări + și incă %d alte notificări + + + %d like + %d likes + %d likes + + Ştergeți notificarea? + Sterge toate notificarile? + Notificarea a fost ștearsă! + Toate notificările au fost sterse! + + Urmăriți + Cei care vă urmăresc + Fixat + + Imposibil de obținut Id-ul clientului! + Unable to connect to instance domain! + Nu există conexiune la Internet! + Contul a fost blocat! + Contul nu mai este blocat! + Contul a fost trecut pe modul silențios! + Contul nu mai este pe modul silențios! + Contul a fost urmat! + Contul nu mai este urmărit! + Notița a fost amplificată! + Notița nu mai este amplificată! + Notița a fost adaugată la favorite! + Notița a fost înlăturată de la favorite! + Notița a fost raportată! + Notița a fost ștearsă! + Notița a fost fixată! + Notița nu mai este fixată! + Oops! A apărut o eroare! + A apărut o eroare! Instanța nu a returnat un cod de autorizare! + Domeniul de instanță nu pare să fie valid! + A apărut o eroare în timp ce schimbați între conturi! + S-a produs o eroare în timpul căutării! + Datele de profil au fost salvate! + Nu se poate efectua nici o acțiune + Media a fost salvată! + S-a produs o eroare în timpul traducerii! + Translations are disabled in settings + Ciornă salvată! + Ești sigur că această instanță permite acest număr de caractere? De obicei, această valoare este aproape de 500 de caractere. + S-a modificat vizibilitatea notițelor pentru contul %1$s + + Număr de notițe pe încărcare + Întotdeauna + WIFI + Întreabă + Încărca media + Încarcă imagini + Arată mai mult… + Show less… + Conţinut sensibil + Disable GIF avatars + Cale: + Salvează ciornele automat + Adauga URL-ul de media în notițe + Notifică atunci când cineva vă urmărește + Notifica atunci când cineva amplifică statusul + Notifica atunci când cineva favorizează statusul tău + Notifică atunci când cineva vă menţionează + Notify when a poll ended + Notify for new posts + Arată caseta de confirmare înainte de amplificare + Arată caseta de confirmare înainte de a adăuga la favorite + Notifică doar în WIFI + Notifică? + Notificații silențioase + NSFW Vezi timp expirare (secunde, 0 înseamnă oprit) + Media Description timeout (seconds, 0 means off) + Editează profilul + Custom sharing + Your custom sharing URL… + Bio… + Lock account + Salvează modificările + Alege poză de profil + Fit preview images + Automatically split toots in replies when chars are over: + Ai atins 160 de caractere permise! + Ai atins 30 de caractere permise! + Între + și + Data trebuie să fie mai mare decât %1$s + Data trebuie să fie mai mică decât %1$s + Oră începerii + Ora terminării + Folosește browser-ul încorporat + Custom tabs + Activează Javascript + Automatically expand cw + Permite cookies terțiare + Your API key, you can leave blank for Yandex + + Dark + Light + Black + + Setează culoarea LED: + + Albastru + Turcoaz + Purpuriu + Verde + Roșu + Galben + Alb + + Urmărește + Deblochează + Mod silențios + Înlătură modul silențios + Cerere trimisă + Vă urmărește + Căutare + First letter in capital for replies + Resize pictures + Resize videos + + Notificări instant + Vă rugăm să confirmați notificările instant pe care doriți să le primiți + Puteți să activați sau dezactivați aceste setări, mai târziu (Fereastra de notificații). + + Goliți memoria cache + Sunt %1$s din date în memoria cache. \n\nVreți să le ștergeți? + Mb + Memoria cache a fost golită! %1$s au fost eliberate + + Title + Title… + Description + Keywords + Keywords… + + Synchronize + Filter + Your toots + Your notifications + Public + Unlisted + Private + Direct + Some keywords… + Show media + Show pinned + No matching result found! + Backup toots for %1$s + %1$s new toots have been imported + %1$s new notifications have been imported + + Dates descending + Dates ascending + + + No + Only + Both + + No toots were found in database. Please, use the synchronize button from the menu to retrieve them. + + Date înregistrate + Doar infomațiile de bază ale conturilor sunt stocate pe aparat. +Aceste date sunt strict confidențiale și pot fi folosite doar de aplicație. +Ștergerea aplicației duce la pierderea datelor.\n +⚠ Autentificarea și parolele nu sunt niciodate stocate. Sunt folosite doar într-o autentificare sigură (SSL). + Permisiuni: + - ACCESS_NETWORK_STATE: Folosit la detectarea daca aparatul este conectat la o rețea de WIFI.\n + - INTERNET: Folosit pentru intergogări la o instanță.\n + - WRITE_EXTERNAL_STORAGE: Folosit pentru a stoca media sau pentru a muta aplicația pe un card SD.\n + - READ_EXTERNAL_STORAGE: Folosit să adauge media la notițe.\n + - BOOT_COMPLETED: Folosit la pornirea serviciului de notificare.\n + - WAKE_LOCK: Folosit în timpul serviciului de notificare. + + Permisiuni API: + - Citește: Citește date.\n + - Scrie: Postează statusuri și încarcă media pentru statusuri.\n + - Urmărește: Urmărește, nu mai urmări, blochează, deblochează.\n\n + ⚠ Aceste acțiuni s efectuează doar la cererea utilizatorului. + + Urmărire și biblioteci + The application does not use tracking tools (audience measurement, error reporting, etc.) and does not contain any advertising.\n\n + The use of libraries is minimized: \n + - Glide: To manage media\n + - Android-Job: To manage services\n + - PhotoView: To manage images\n + + Traducerea notițelor + Alpicația oferă posibilitatea de a traduce notițe folosind local-ul aparatului și API-ul Yandex.\n + Yandex are propria politică de confidențialitate care poate fi găsită aici: + https://yandex.ru/legal/confidential/?lang=en + Vă mulțumesc: + + Filtrează expresii usuale + Căutare + Șterge + Fetch more toots… + + Lists + Are you sure you want to permanently delete this list? + There is nothing in this list yet. When members of this list post new statuses, they will appear here. + Add to list + Add list + Delete list + Edit list + New list title + The account was added to the list! + You don\'t have any lists yet! + + %1$s has moved to %2$s + Authentication does not work? + Here are some checks that might help:\n\n + - Check there is no spelling mistakes in the instance name\n\n + - Check that your instance is not down\n\n + - If you use the two-factor authentication (2FA), please use the link at the bottom (once the instance name is filled)\n\n + - You can also use this link without using the 2FA\n\n + - If it still does not work, please raise an issue on FramaGit at https://framagit.org/tom79/fedilab/issues + + Media has been loaded. Tap here to display it. + This action can be quite long. You will be notified when it will be finished. + Still running, please wait… + Export statuses + Export statuses for %1$s + %1$s toots out of %2$s have been exported. + Something went wrong when exporting data for %1$s + Something went wrong when exporting data! + Something went wrong when importing data! + + Proxy + Enable proxy? + Host + Port + Login + Password + Add toot details when sharing + Support the app on Liberapay + There is an error in the regular expression! + No timelines was found on this instance! + Delete this instance? + Translate in + Follow instance + You already follow this instance! + The instance is followed! + Partnerships + Information + Hide boosts from %s + Feature on profile + Show boosts from %s + Don\'t feature on profile + The account is now featured on profile + The account is no longer featured on profile + Boosts are now shown! + Boosts are now hidden! + Direct message + Filters + No filters to display. You can create one by tapping on the \"+\" button. + Keyword or phrase + Home timeline + Public timelines + Notifications + Conversations + Will be matched regardless of casing in text or content warning of a toot + Drop instead of hide + Filtered toots will disappear irreversibly, even if filter is later removed + When the keyword or phrase is alphanumeric only, it will only be applied if it matches the whole word + Whole word + Filter contexts + One or multiple contexts where the filter should apply + Expire after + Delete filter? + Update filter + Create filter + Whom to follow + There is no accounts listed for the moment! + Follow + Select all + Unselect all + %s is followed! + Creating the list %s + Adding accounts to the list + Accounts were added to the list + Adding accounts to the list + You have not created a list yet. Tap on the \"+\" button to add a new one. + Who to follow + Trunk API + Account(s) can\'t be followed + Fetching remote account + Automatically expand hidden media + New follow + New Boost + New Favourite + New Mention + Poll Ended + New Toot + Toots Backup + New posts + Media Download + Change notification sound + Select Tone + Enable time slot + How To Videos + Fetching remote thread! + No blocked domains! + Unblock domain + Are you sure to unblock %s? + Are you sure to block %s?\n\nYou will not see any content from that domain in any public timeline or in your notifications. Your followers from that domain will be removed. + Blocked domains + Block domain + The domain is blocked + The domain is no longer blocked! + Fetching remote status + Comment + Peertube instance + Be the first to leave a comment on this video with the top right button! + %s views + Duration: %s + Add an instance + Comments are not enabled on this video! + Pick up a resolution + Peertube favourites + The video has been added to bookmarks! + The video has been removed from bookmarks! + There is no Peertube videos in your favourites! + Channel + Videos + Channels + Use Emoji One + Information + Display previews in all toots + New UX/UI designer + Display video previews + The account id has been copied in the clipboard! + Change the language + Default language + Truncate long toots + Truncate toots over \'x\' lines. Zero means disabled. + Display more + Display less + Manage tags + The tag already exists! + The tag has been stored! + The tag has been changed! + The tag has been removed! + Schedule boost + The boost is scheduled! + No scheduled boost to display! + Schedule boost.]]> + Art timeline + Open menu + Go back + Logo of the application + Profile picture + Profile banner + Contact admin of the instance + Add new + MastoHost logo + Emoji picker + Refresh + Expand the conversation + Remove an account + Remove the blocked domain + Custom emoji picker + Play video + New toot + Image of the card + Hide media + Favicon + Add description for media (for the visually impaired) + + Never + 30 minutes + 1 hour + 6 hours + 12 hours + 1 day + 1 week + + In this field, you need to write your instance host name.\nFor example, if you created your account on https://mastodon.social\nJust write mastodon.social (without https://)\n + You can start writing first letters and names will be suggested.\n\n + ⚠ The Login button will only work if the instance name is valid and the instance is up! + + More information + + Languages + Media only + Show NSFW + Crowdin translations + Crowdin manager + Translation of the application + About Crowdin + Bot + Pixelfed instance + Mastodon instance + Any of these + All of these + None of these + Any of these words (space-separated) + All these words (space-separated) + Add some words to filter (space-separated) + Change column name + Misskey instance + No app supporting this link is installed on your device. + Subscriptions + Overview + Trending + Recently added + Local + Upload + Reply + Delete a comment + Are you sure to delete this comment? + Full screen video + Mode for videos + Select the file to upload + My videos + Title + License + Category + Language + This video contains mature or explicit content + Enable video comments + Update video + Description + The video has been updated! + Upload cancelled! + The video has been uploaded! + Uploading, please wait… + Tap here to edit the video data. + Delete video + Are you sure to delete this video? + Display NSFW videos + No videos to display! + Leave a comment + Share + Choose a schedule mode + From device + From server + Toots (Server) + Toots (Device) + Modify + Display new toots above the \"Fetch more\" button + Timelines + Interface + Contacts + %1$s commented your video %2$s]]> + %1$s is following your channel %2$s]]> + %1$s is following your account]]> + %1$s has been published]]> + %1$s succeeded]]> + %1$s failed]]> + %1$s published a new video: %2$s]]> + %1$s has been blacklisted]]> + %1$s has been unblacklisted]]> + Export data + Import Data + Select the file to import + An error occurred when selecting the backup file! + Add a public comment + Send comment + There is no Internet connection. Your message has been stored in drafts. + Plain text + HTML + Markdown + Logout account + All + Support the app + Open Collective enables groups to quickly set up a collective, raise funds and manage them transparently. + Copy link + Connect + Normal + Compact + Console + Set display mode + Patch the Security Provider + Update tracking domains + The tracking data base has been updated! + http calls blocked by the application + List of blocked calls + Submit + The data base has been exported! + Featured hashtags + Filter timeline with tags + No tags + Hide the \"delete\" button in the notification tab + Attach an image when sharing a URL + + Poll + Polls + Create a poll + Choice 1 + Choice 2 + Choice %d + You need two choices at least for the poll! + Done + end at %s + Refresh poll + Vote + A poll you have voted in has ended + A poll you tooted has ended + Customize + Categories + Time slot + Advanced + Display \'new\' badge on unread toots + Peertube + Move timeline + Hide timeline + Reorder timelines + List permanently deleted + Followed instance removed + Pinned tag removed + Undo + You need to keep two visible tabs! + Reorder timelines + Main timelines can only be hidden! + BBCode + Always mark media as sensitive + GNU instance + Cached status + Forward tags in replies + Long press to store media + Blur sensitive media + Display timelines in a list + Display timelines + Mark bot accounts in toots + Manage tags + Remember the position in Home timeline + History + Playlists + Display name + You don\'t have any playlists. Tap on the \"+\" icon to add a new playlist + You must provide a display name! + The channel is required when the playlist is public. + Create a playlist + There is nothing in this playlist yet. + redo + Gallery + Emoji + Sticker + Eraser + Text + Filter + Brush + Are you sure you want to exit without saving the image? + Discard + Saving… + Image Saved Successfully! + Failed to save Image + Opacity + Enable photo editor + Add a poll item + Remove last poll item + Mute conversation + Unmute conversation + The conversation is no longer muted! + The conversation is muted + Open application features + Timed mute + Mention the account + Refresh cache + Mention the status + News + General + Regional + Art + Journalism + Activism + Gaming + Technology + Adult content + Furry + Food + Logo of the instance + Something went wrong when checking available instances! + Join Mastodon + Choose an instance by picking up a category, then tap on a check button. + Choose an instance by tapping on a check button. + %1$s users + Confirm password + I agree to %1$s and %2$s + server rules + terms of service + Sign up + This instance works with invitations. Your account will need to be manually approved by an administrator before being usable. + Please, fill all the fields! + Passwords don\'t match! + The email doesn\'t seem to be valid! + Your username will be unique on %1$s + You will be sent a confirmation e-mail + Use at least 8 characters + Password should contain at least 8 characters + Username should only contain letters, numbers and underscores + Account created! + Your account has been created!\n\n + Think to validate your email within the 48 next hours.\n\n + You can now connect your account by writing %1$s in the first field and click on Connect.\n\n + Important: If your instance required validation, you will receive an email once it is validated! + + Save the message in drafts? + Administration + Reports + No reports to display! + Reconnect the account + The application failed to access the admin features. You might need to reconnect the account for having the correct scope. + Unresolved + Remote + Active + Pending + Disabled + Silenced + Suspended + Permissions + Email status + Login status + Joined + Most recent IP + Warn + Disable + Silence + Notify the user per e-mail + Custom warning + User + Moderator + Administrator + Confirmed + Not confirmed + Reported statuses + Account + Undo silence + Undo disable + Suspend + Undo suspend + The account is silenced! + The account is no longer silenced! + The account is suspended! + The account is no longer suspended! + The account is disabled! + The account is no longer disabled! + The account has been warned! + Display the admin menu + Display the admin feature in statuses + Allow + The account is approved! + The account is rejected! + Assign to me + Unassign + Mark as resolved + Mark as unresolved + Empty content! + Display Fedilab features button + The application needs to access audio recording + Voice message + Enable quick reply + The account you are replying might not see your message! + If disabled, the app will always load last statuses + If disabled, sensitive media will be hidden with a button + Store media in full size with a long press on previews + Add an ellipse button at the top right for listing all tags/instances/lists + During the time slot, the app will send notifications. You can reverse (ie: silent) this time slot with the right spinner. + Display a Fedilab button below profile picture. It is a shortcut for accessing in-app features. + Allow to reply directly in timelines below statuses + Previews will not be cropped in timelines + Allow to play embedded videos directly in timelines + Allow to reverse the way to read statuses that are displayed once tapping the fetch more button + This option allows to support recent cipher suites. It is useful for older Android devices or if you cannot connect to your instance. + Exclusively for Peertube videos. Switch this mode if you cannot play them. + These tags will allow to filter statuses from profiles. You will have to use the context menu for seeing them. + Automatically insert a line break after the mention to capitalize the first letter + Allow content creators to share statuses to their RSS feeds + Compose + Maximum retry times when uploading media + Create a new Folder here + Enter the folder name + Please enter a valid folder name + This folder already exists.\n Please provide another name for the folder + Select + Default Directory + Folder + Create folder + Display a toast message after an action has been completed (boost, fav, etc.)? + Muted instances have been exported! + Add an instance + Export instances + Import instances + Crash reports + Enable crash reports + If enabled, a crash report will be created locally and then you will be able to share it. + Fedilab has stopped :( + You can send me by email the crash report. It will help to fix it :)\n\nYou can add additional content. Thank you! + Use the wysiwyg + When enabled, you will be able to format your text easily with tools. + Statistics + Total statuses + Number of boosts + Number of favourites + Number of mentions + Number of follows + Number of polls + Number of replies + Number of statuses + Statuses + Visibility + Number with media + Number with sensitive media + Number with CW + First status date + Last status date + First notification date + Last notification date + Frequency + %s statuses per day + %s notifications per day + Date range + Groups + No groups! + Disable custom animated emojis + Charts + Display charts + The application collects your local data, please wait… + Backup + Auto backup statuses + This option is per account. It will launch a service that will automatically store your statuses locally in the database. That allows to get statistics and charts + Auto backup notifications + This option is per account. It will launch a service that will automatically store your notifications locally in the database. That allows to get statistics and charts + Report account + Send an invitation + Your instance does not allow to register a new account! + + %d vote + %d votes + %d votes + + + %d voter + %d voters + %d voters + + + Single choice + Multiple choices + + + 5 minutes + 30 minutes + 1 hour + 6 hours + 1 day + 3 days + 7 days + + + Webview + Direct stream + + For joining my instance \"%1$s\", you can download Fedilab:\n\nF-Droid: %2$s\nGoogle: %3$s\n\nThen open the link below with Fedilab and create your account :)\n\n%4$s + + Your poll can\'t have duplicated options! + For all accounts + Database cache + Clear your home timeline cache + Clear your cached statuses + Clear your bookmarks + Files in cache + Total notifications + Hide menu items + Fedilab is running live notifications + For %1$s accounts with %2$s events + Live notifications for %1$s + Live notifications will be enabled for this account. + Clear cache when leaving + The cache (media, cached messages, data from the built-in browser) will be automatically cleared when leaving the application. + Do you want to unfollow this account? + Show confirmation dialog before unfollowing + Replace Youtube with Invidio.us + Invidious is an alternative front-end to YouTube + Enter your custom host or leave blank for using invidious.snopyta.org + Replace Twitter with Nitter + Nitter is an open source alternative Twitter front-end focused on privacy. + Enter your custom host or leave blank for using nitter.net + Replace Instagram with Bibliogram + Bibliogram is an open source alternative Instagram front-end focused on privacy. + Enter your custom host or leave blank for using bibliogram.art + Replace Reddit with Libreddit + Libreddit is an open source alternative Reddit front-end focused on privacy. + Enter your custom host or leave blank for using libredd.it + Replace Medium links + Replace medium.com links with an open source alternative front-end focused on privacy. + Default: scribe.rip + Replace Wikipedia links + Replace Wikipedia link with an open source alternative front-end focused on privacy. + Default: wikiless.org + Hide Fedilab notification bar + For hiding the remaining notification in the status bar, tap on the eye icon button then uncheck: \"Display in status bar\" + Use a push notifications system for getting notifications in real time. + No live notifications + Live notifications + Notifications will be fetched every 15 minutes. + Add notes + Notes for the account + Allow to compress large photos into smaller sized photos with very less or negligible loss in quality of the image. + Allow compressing videos while maintaining their quality. + The app is compressing the media, it can be quite long… + Change app icon + Tap to change the app icon + Post + Visibility of the post + Tap here to add photos + Accepted Formats: jpeg, png, gif \n\nMax File Size: 15 MB \n\nAlbums can contain up to 4 photos or videos + Upload media + Add an optional caption + The app received a very long error message from the API %1$s + Message preview + Add mentions in each message + Fetching conversation + Order by + Title for the video + Join Peertube + I am at least 16 years old and agree to the %1$s of this instance + Links + Change the color of links (URLs, mentions, tags, etc.) in messages + Reblogs header + Change the color of display name at the top of messages + Change the color of the user name at the top of messages + Change the color of the header for reblogs + Posts + Background color of posts in timelines + Reset colors + Tap here to reset all your custom colors + Reset + Icons + Color of bottom icons in timelines + Pin this tag + Logo of the instance + Edit profile + Make an action + Translation + Image preview + Text color + Change the text color in pots + Apply changes + You need to restart the application to apply changes + Restart + Use a custom theme + Allow to override colors of the selected theme above + Theming + Store before + The theme was exported + The theme has been successfully exported in CSV + Apply the primary color to the status bar + Status bar color + Restore a default theme + Import a theme + Tap here to import a theme from a previous export + Export the theme + Tap here to export the current theme + An error occurred when selecting the theme file + Theme Picker + Select a pre-installed theme + Themes + Apply the primary color to the navigation bar + Navigation bar color + The underlying color of the app’s content. + Background color + Accents select parts of the UI. + Accent color + Displayed most frequently across your app. + Primary color + Export bookmarks to the instance + Import bookmarks from the instance + User count + Status count + Instance count + Blocked + End in %s + What\'s new in %s + You can follow my account for updates + This instance is not available on https://instances.social + Display full link + Share link + The URL has been copied to the clipboard + Open with another app + Check redirect + This URL does not redirect + %1$s \n\nredirects to\n\n %2$s + Change the user agent + Set a custom user agent or leave blank + Allows to customize the user agent used for api calls or with the built-in browser. + Remove UTM parameters + The app will automatically remove UTM parameters from URLs before visiting a link. + Trends + Trending now + %d people talking + Twitter accounts (via Nitter) + Twitter usernames space separated + Identity proofs + Verified identity + Verified by %1$s (%2$s) + Delete the notification + Display more options + It is a Pixelfed story + Upload a media, it will be automatically added to your Pixelfed story. + Media successfully added to your story! + Action disabled + Unfollow + Something went wrong, please check your download directory in settings. + Announcements + No announcements! + Add a reaction + Use your favourite browser inside the app. Uncheck this feature to open links externally. + Video cache in MB, zero means no cache. + Watermarks + Automatically add a watermark at the bottom of pictures. The text can be customized for each account. + No distributors found! + You need a distributor for receiving push notifications.\nYou will find more details at %1$s.\n\nYou can also disable push notifications in settings for ignoring that message. + Select a distributor + diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml new file mode 100644 index 000000000..8ad7166c8 --- /dev/null +++ b/app/src/main/res/values-ru/strings.xml @@ -0,0 +1,1158 @@ + + + Открыть меню + Закрыть меню + О программе + Об этом инстансе + Конфиденциальность + Кэш + Выход + Войти + + Закрыть + Да + Нет + Отмена + Скачать + Скачать %1$s + Медиафайл сохранен + Файл: %1$s + Пароль + Email + Аккаунты + Туты + Теги + Сохранить + Восстановить + Нет результатов! + Инстанс + Инстанс: mastodon.social + Теперь работает с аккаунтом %1$s + Добавление аккаунта + Содержимое тута было скопировано в буфер обмена + URL тута был скопирован в буфер обмена + Изменить + Выбрать изображение… + Очистить + Камера + Удалить все + Перевести этот тут. + Запланировать + Размеры текста и значков + Изменить текущий размер текста: + Изменить текущий размер значка: + Следующий + Предыдущий + Открыть с помощью + Подтвердить + Медиа + Поделиться через + Поделился через Fedilab + Ответы + Имя пользователя + Черновики + Избранные + Новые подписчики + Упоминания + Продвинули + Показать продвинутые + Показать ответы + Открыть в браузере + Перевести + Пожалуйста, подождите несколько секунд, прежде чем выполнить это действие. + + Домой + Местная лента + Федеративная лента + Опции + В избранном + Общение + Игнорируемые пользователи + Заблокированные пользователи + Уведомления + Запросы на подписку + Настройки + Удалить аккаунт + Удалить аккаунт %1$s из приложения? + Отправить email + Нажмите на путь, чтобы изменить его + Ошибка! + Запланированные туты + Информация, приведенная ниже, может не соответствовать профилю пользователя. + Вставить эмодзи + На данный момент приложение не собирает пользовательские эмодзи. + Push-уведомления + Вы уверены, что хотите выйти? + Вы уверены, что хотите выйти @%1$s@%2$s? + + Нет тутов для отображения + Нет историй для отображения + Истории + Продвинут %1$s + Добавить этот тут в избранное? + Удалить этот тут из избранного? + Продвинуть этот тут? + Не продвигать этот тут? + Закрепить этот тут? + Открепить этот тут? + Игнорировать + Заблокировать + Пожаловаться + Удалить + Копировать + Поделиться + Упомянуть + Временно игнорировать + Удалить & переписать + + Игнорировать этот аккаунт? + Заблокировать этот аккаунт? + Пожаловаться на этот тут? + Заблокировать этот домен? + Не игнорировать этот аккаунт? + Разблокировать этот аккаунт? + + + Уведомлять + Беззвучный + + + Удалить этот тут? + Удалить & переписать этот тут? + + Закладки + Добавить в закладки + Удалить закладку + Нет закладок для отображения + Статус добавлен в закладки! + Статус удален из закладок! + + %d с + %d м + %d ч + %d д + + %d секунда + %d секунды + %d секунд + %d секунды + + + %d минута + %d минуты + %d минут + %d минуты + + + %d час + %d часа + %d часов + %d часа + + + %d день + %d дня + %d дней + %d дня + + + Предупреждение + О чем вы думаете? + ТРУБИТЬ! + КВИТ! + 18+ + Написать тут + Ответ на тут + Написать квит + Ответить на квит + Выбрать медиафайл + При выборе медиафайла произошла ошибка! + Удалить этот медиафайл? + Ваш тут пуст! + Видимость тута + Видимость тутов по умолчанию: + Тут отправлен! + Вы отвечаете на этот тут: + Деликатный контент? + + Публиковать в публичных лентах + Не публиковать в публичных лентах + Публиковать только для подписчиков + Публиковать только упомянутым пользователям + + Черновиков нет! + Выбрать тут + Выберите аккаунт + Выберите аккаунты + Удалить черновик? + Нажмите кнопку, чтобы отобразить исходный тут + Опишите для слабовидящих + + Описание недоступно! + + Версия %1$s + Разработчик: + Лицензия: + GNU GPL V3 + Исходный код: + Перевод тутов: + Поиск инстансов: + Дизайнер иконки: + + Разговор + + Никого + Нет запросов на подписку + Туты \n %1$s + Подписки \n %1$s + Подписчики \n %1$s + Закреплены \n %d + Авторизовать + Отклонить + + Нет запланированных тутов для отображения! + Напишите тут, а затем выберите Запланировать из верхнего меню. + Удалить запланированный тут? + Медиафайл: %d + Тут запланирован! + Запланированная дата должна быть в будущем! + Режим энергосбережения включен! Возможна некорректная работа. + + Время игнорирования должно быть больше одной минуты. + %1$s теперь игнорируется до %2$s.\n Вы можете изменить это, посетив ее/его профиля. + %1$s игнорируется до %2$s.\n Нажмите здесь, чтобы изменить это. + + Нет уведомлений для отображения + упомянул(а) вас + написал новое сообщение + ваш статус продвинут + добавил ваш статус в избранные + подписался на вас + попросил подписаться на вас + + и другое уведомление + и %d других уведомления + и %d других уведомлений + и %d других уведомлений + + + %d лайк + %d лайка + %d лайков + %d лайков + + Удалить уведомление? + Удалить все уведомления? + Это уведомление было удалено! + Все уведомления были удалены! + + Подписки + Подписчики + Закреплено + + Не удалось получить идентификатор клиента! + Невозможно подключиться к домену инстанса! + Нет подключения к Интернету! + Аккаунт заблокирован! + Аккаунт больше не блокируется! + Этот аккаунт теперь игнорируется! + Этот аккаунт больше не игнорируется! + Вы подписались на этот аккаунт! + Вы больше не подписаны на этот аккаунт! + Этот тут был продвинут! + Этот тут больше не продвигается! + Этот тут был добавлен в ваше избранное! + Этот тут был удален из вашего избранного! + Жалоба на этот тут отправлена! + Этот тут был удален! + Этот тут был закреплен! + Этот тут был откреплен! + Упс ! Произошла ошибка! + Произошла ошибка! Инстанс не вернул код авторизации! + Этот домен инстанса недействителен! + При переключении между аккаунтами произошла ошибка! + Произошла ошибка при поиске! + Данные профиля сохранены! + Ничего нельзя сделать + Этот медиафайл сохранен! + При переводе произошла ошибка! + Переводы отключены в настройках + Черновик сохранен! + Вы уверены, что данный сервер допускает такое количество знаков? Обычно это значение близко к 500. + Видимость тутов была изменена для аккаунта %1$s + + Количество тутов на загрузку + Всегда + WIFI + Спрашивать + Загружать медиа + Загружать изображения + Еще… + Свернуть… + Деликатный контент + Отключить GIF-аватары + Путь: + Сохранять черновики автоматически + Добавлять URL-адрес медиа в туты + Уведомлять, когда кто-то подписывается на вас + Уведомлять, когда кто-то продвигает ваш статус + Уведомлять, если кто-то добавил ваш статус в избранное + Уведомлять, когда кто-то вас упоминает + Уведомлять об окончании опроса + Уведомлять о новых записях + Предупреждать перед тем как продвинуть + Предупреждать перед добавлением в избранные + Уведомлять только в сети WIFI + Уведомлять? + Тихие уведомления + Время просмотра NSFW (в секундах, 0 означает выключено) + Время просмотра описания (в секундах, 0 означает выключено) + Изменить профиль + Пользовательский обмен + Ваш пользовательский URL для обмена… + О… + Заблокировать аккаунт + Сохранить изменения + Выбрать изображение обложки + Подгонять изображения предпросмотра + Автоматически разделять туты, содержащие более 500 символов + Вы достигли 160 символов! + Вы достигли 30 символов! + Между + и + Время должно быть больше, чем %1$s + Время должно быть меньше, чем %1$s + Время начала + Время окончания + Использовать встроенный браузер + Внешний браузер + Включить Javascript + Автоматически раскрывать 18+ + Разрешить сторонние куки + Ваш ключ API (для Яндекса можно оставить пустым) + + Темная + Светлая + Черная + + Цвет светодиода: + + Синий + Бирюзовый + Малиновый + Зеленый + Красный + Желтый + Белый + + Подписаться + Разблокировать + Игнорировать + Не игнорировать + Запрос отправлен + Подписан на вас + Поиск + Заглавная первая буква в ответах + Изменять размер изображений + Изменить размер видео + + Push-уведомления + Пожалуйста, подтвердите push-уведомления, которые вы хотите получать. + Вы можете включить или отключить эти уведомления позже в настройках (вкладка \'Уведомления\'). + + + Очистка кэша + Данными кэша занято %1$s.\n\nВы хотите их удалить? + МБ + Кэш очишен! Освобождено %1$s + + Название + Название… + Описание + Ключевые слова + Ключевые слова… + + Синхронизировать + Фильтр + Ваши туты + Ваши уведомления + Публично + Не в списке + Приватно + Напрямую + Некоторые ключевые слова… + Показать медиа + Показать закрепленные + Результатов не найдено! + Резервное копирование тутов для %1$s + было импортировано %1$s новых тутов + было импортировано %1$s новых уведомлений + + Даты по убыванию + Даты по возрастанию + + + Нет + Только + Оба + + В базе данных тутов не найдено. Пожалуйста, используйте кнопку синхронизации в меню для их получения. + + Собираемые данные + На устройстве хранятся только основные сведения из аккаунтов. + Эти данные являются строго конфиденциальными и могут использоваться только приложением. + Удаление приложения немедленно удалит эти данные.\n + ⚠ Логин и пароли никогда не сохраняются. Они используются только во время безопасной аутентификации (SSL) с инстансом. + + Разрешения: + - ACCESS_NETWORK_STATE: Используется для определения, подключено ли устройство к сети WIFI.\n + - INTERNET: Используется для запросов к инстансу.\n + - WRITE_EXTERNAL_STORAGE: Используется для хранения мультимедиа или для перемещения приложения на SD-карту.\n + - READ_EXTERNAL_STORAGE: Используется для добавления мультимедиа в туты.\n + - BOOT_COMPLETED: Используется для запуска службы уведомлений.\n + - WAKE_LOCK: Используется во время службы уведомлений. + + Разрешения API: + - Чтение: Чтение данных.\n + - Запись: Публикация статусов и загрузка файлов для статусов.\n + - Подписка: Подписка, отписка, блокировка, разблокировка.\n\n + ⚠ Эти действия выполняются только при запросе их пользователем. + + Отслеживание и библиотеки + Это приложение не использует инструменты отслеживания (измерение аудитории, отчет об ошибках и т. д.) и не содержит никакой рекламы.\n\n + Использование библиотек сведено к минимуму: \n + - Glide: Для управления файлами\n + - Android-Job: Для управления сервисами\n + - PhotoView: Для управления изображениями\n + + Перевод тутов + Приложение предлагает возможность переводить туты, используя язык устройства и Яндекс API.\n + Яндекс имеет собственную политику конфиденциальности, условия которой можно найти здесь: https://yandex.ru/legal/confidential/?lang=en + + Спасибо: + + Отфильтровать по регулярным выражениям + Поиск + Удалить + Получить больше тутов… + + Списки + Вы действительно хотите удалить этот список навсегда? + В этом списке пока ничего нет. Когда члены этого списка опубликуют новые статусы, они появятся здесь. + Добавить в список + Добавление списка + Удалить список + Изменить список + Название нового списка + Аккаунт был добавлен в список! + У вас еще нет списков! + + %1$s был перенесен в %2$s + Аутентификация не работает? + Вот некоторые подсказки, которые могут помочь:\n\n + - Проверьте, нет ли орфографических ошибок в имени инстанса\n\n + - Убедитесь в доступности вашего инстанса\n\n + - Если вы используете двухфакторную аутентификацию (2FA), используйте ссылку внизу (после заполнения имени сервера)\n\n + - Вы также можете использовать эту ссылку без 2FA\n\n + - Если все перечисленное вам не помогло, создайте проблему на Framagit: https://framagit.org/tom79/fedilab/issues + + Медиафайл загружен. Нажмите здесь, для его отображения. + Это действие может продолжаться довольно долго. Вы будете уведомлены, когда оно будет завершено. + Все еще работает, подождите… + Экспорт статусов + Экспорт статусов для %1$s + %1$s тутов из %2$s были экспортированы. + При экспорте данных для %1$s что-то пошло не так + При экспорте данных для что-то пошло не так! + При экспорте данных для что-то пошло не так! + + Прокси + Включить прокси? + Хост + Порт + Имя пользователя + Пароль + Добавлять детали тута при перепосте + Поддержать приложение на Liberapay + В регулярном выражении есть ошибка! + На этом инстансе лент не найдено! + Удалить этот инстанс? + Перевести на + Подписаться на инстанс + Вы уже подписаны на этот инстанс! + Вы подписаны на этот инстанс! + Партнерство + Информация + Скрыть продвигаемые от %s + Отображать в профиле + Показать продвигаемые от %s + Не показывать в профиле + Этот аккаунт теперь отображается в профиле + Этот аккаунт больше не отображается в профиле + Продвигаемые теперь отображаются! + Продвигаемые теперь скрываются! + Прямое сообщение + Фильтры + Нет фильтров для отображения. Вы можете создать первый, нажав \"+\". + Ключевое слово или фраза + Домашняя лента + Публичные ленты + Уведомления + Разговоры + Сравнение будет выполнено независимо от регистра текста и предупреждения о содержании тута + Удалить вместо скрытия + Отфильтрованные туты исчезнут необратимо, даже если фильтр будет позже удален + Если ключевое слово или фраза состоит из букв и цифр, он будет применен только если совпадает со словом целиком + Слово целиком + Фильтр контекстов + Один или несколько контекстов, в которых должен быть применен фильтр + Истекает после + Удалить фильтр? + Обновить фильтр + Создать фильтр + На кого подписаться + В настоящее время аккаунтов в списке нет! + Подписаться + Выбрать все + Снять выбор + %s подписан! + Создание списка %s + Добавление аккаунтов в список + Аккаунты были добавлены в список + Добавление аккаунтов в список + Вы еще не создали список. Нажмите \"+\", чтобы добавить первый. + На кого подписаться + Trunk API + Аккаунт(ы) не могут быть подписаны + Получение внешнего аккаунта + Показать скрытые медиа автоматически + Новая подписка + Новое продвижение + Новое избранное + Новое упоминание + Опрос закончен + Новый тут + Резервирование тутов + Новые сообщения + Загрузка медиа + Изменить звук уведомления + Выбрать сигнал + Активировать временной интервал + Видеоинструкции + Получение внешней ветки! + Нет заблокированных доменов! + Разблокировать домен + Вы уверены, что хотите разблокировать %s? + Вы уверены, что хотите заблокировать %s? + Заблокированные домены + Заблокировать домен + Этот домен заблокирован + Этот домен больше не заблокирован! + Получение внешнего статуса + Комментарий + Инстанс Peertube + Будьте первым прокомментировавшим это видео (при помощи верхней правой кнопки)! + %s просмотров + Продолжительность: %s + Добавить инстанс + Комментирование этого видео отключено! + Подобрать разрешение + Избранное Peertube + Видео добавлено в закладки! + Видео удалено из закладок! + В вашем избранном нет видео Peertube! + Канал + Видео + Каналы + Использовать Emoji One + Информация + Отображать превью для всех тутов + Новый дизайнер UX/UI + Отображать превью видео + Идентификатор учетной записи скопирован в буфер обмена! + Изменить язык + Язык по умолчанию + Усекать длинные туты + Усекать туты более \'x\' строк. Ноль означает отключено. + Развернуть + Свернуть + Управление тегами + Тег уже существует! + Тег был сохранен! + Тег был изменен! + Тег был удален! + Расписание продвижения + Продвижение запланировано! + Нет запланированных продвижений для отображения! + Запланировать продвижение.]]> + Лента Art + Открыть меню + Назад + Логотип приложения + Изображение профиля + Баннер профиля + Связаться с администратором инстанса + Добавить новое + Логотип MastoHost + Выбор Emoji + Обновить + Развернуть разговор + Удалить аккаунт + Удалить заблокированный домен + Персонализация выбора Emoji + Воспроизвести видео + Новый тут + Изображение карты + Скрыть медиа + Значок сайта + Добавить описание для медиафайла (для слабовидящих) + + Никогда + 30 минут + 1 час + 6 часов + 12 часов + 1 день + 1 неделя + + В этом поле необходимо указать имя хоста вашего инстанса.\nНапример, если вы создали учетную запись на https://mastodon.social\nвведите mastodon.social (без https://)\n + Начните писать, чтобы получить предложения.\n\n + ⚠ Кнопка входа будет работать только в случае, если инстанс включен и его имя корректно! + + Дополнительная информация + + Языки + Только медиа + Показать NSFW + Переводы Crowdin + Менеджер Crowdin + Перевод приложения + О Crowdin + Бот + Инстанс Pixelfed + Инстанс Mastodon + Любой из этих + Все из этих + Ни один из этих + Любое из этих слов (через пробел) + Все эти слова (через пробел) + Добавьте слова в фильтр (через пробел) + Изменить имя столбца + Инстанс Misskey + На вашем устройстве не установлено приложение, для открытия этой ссылки. + Подписки + Обзор + В тренде + Недавно добавленные + Локальные + Загрузить + Ответ + Удалить комментарий + Вы уверены, что хотите удалить этот комментарий? + Полноэкранное видео + Режим видео + Выберите файл для загрузки + Мои видео + Название + Лицензия + Категория + Язык + Это видео содержит откровенный контент или контент для взрослых + Включить комментарии к видео + Обновить видео + Описание + Видео было обновлено! + Загрузка отменена! + Видео было загружено! + Загрузка, пожалуйста, подождите… + Нажмите здесь, чтобы редактировать данные видео. + Удалить видео + Вы уверены, что хотите удалить это видео? + Отображать видео NSFW + Нет видео для отображения! + Оставить комментарий + Поделиться + Выберите режим расписания + С устройства + С сервера + Туты (Сервер) + Туты (Устройство) + Изменить + Отображать новые туты над кнопкой \'Получить больше\' + Ленты + Интерфейс + Контакты + %1$s прокомментировал ваше видео %2$s]]> + %1$s следит за вашим каналом %2$s]]> + %1$s отслеживает ваш аккаунт]]> + %1$s было опубликовано]]> + %1$s успешно импортировано]]> + %1$s произошла ошибка]]> + %1$s опубликовал новое видео: %2$s]]> + %1$s занесено в черный список]]> + %1$s удалено из черного списка]]> + Экспорт данных + Импорт данных + Выберите файл для импорта + Произошла ошибка при выборе файла резервной копии! + Добавить публичный комментарий + Отправить комментарий + Нет подключения к интернету. Ваше сообщение было сохранено в черновиках. + Простой текст + HTML + Markdown + Выйти из аккаунта + Все + Поддержать это приложение + Open Collective позволяет группам быстро создавать коллектив, собирать средства и прозрачно управлять ими. + Копировать ссылку + Подключиться + Нормальный + Компактный + Консольный + Установить режим отображения + Улучшение безопасности + Обновить отслеживаемые домены + База данных отслеживания была обновлена! + http-вызовы, заблокированные приложением + Список заблокированных вызовов + Подтвердить + База данных была экспортирована! + Рекомендуемые хэштеги + Фильтровать ленту тегами + Нет тегов + Скрыть кнопку удаления на вкладке уведомлений + Прикреплять изображение при перепосте URL + + Опрос + Опросы + Создать опрос + Вариант 1 + Вариант 2 + Вариант %d + Необходимо добавить не менее двух вариантов для создания опроса! + Готово + заканчивается в %s + Обновить опрос + Голосовать + Опрос, в котором вы проголосовали, завершен + Один из ваших опросов завершен + Настроить + Категории + Временной интервал + Дополнительно + Показывать значок \'new\' на непрочитанных тутах + Peertube + Переместить ленту + Скрыть ленту + Изменить порядок лент + Список окончательно удален + Отслеживаемый инстанс удален + Закрепленный тег удален + Вернуть + Необходимо сохранить две видимые вкладки! + Изменение порядка лент + Основные ленты могут быть только скрыты! + BBCode + Всегда отмечать медиафайлы как деликатный контент + Инстанс GNU + Состояние кэша + Пересылать теги в ответах + Длительное нажатие сохраняет медиа + Размытие деликатного контента + Показывать ленты в списке + Отображать ленты + Отметить аккаунты ботов в тутах + Управлять тегами + Запоминать положение в домашней ленте + История + Плейлисты + Отображаемое имя + У вас нет плейлистов. Нажмите на \"+\", чтобы добавить новый + Вы должны указать отображаемое имя! + Этот канал необходим, когда плейлист общедоступен. + Создать плейлист + В этом плейлисте пока ничего нет. + вернуть + Галерея + Эмодзи + Стикер + Ластик + Текст + Фильтр + Кисть + Вы уверены, что хотите выйти без сохранения изображения? + Сбросить + Сохранение… + Изображение сохранено успешно! + Не удалось сохранить изображение + Затенение + Включить редактор фотографий + Добавить элемент опроса + Удалить последний элемент опроса + Игнорировать разговор + Не игнорировать разговор + Этот разговор больше не игнорируется! + Этот разговор теперь игнорируется + Открыть настройки приложения + Временно игнорировать + Упомянуть аккаунт + Обновить кэш + Упомянуть статус + Новости + Основной + Региональный + Искусство + Журналистика + Активизм + Игры + Технологии + Контент для взрослых + Пушистый + Еда + Логотип инстанса + Что-то пошло не так при проверке доступных инстансов! + Присоединиться к Mastodon + Выберите инстанс, подобрав категорию и нажмите кнопку проверки. + Выберите инстанс, нажав на кнопку проверки. + %1$s пользователей + Подтвердить пароль + Я принимаю %1$s и %2$s + правила сервера + условия обслуживания + Регистрация + Этот инстанс работает по приглашениям. Ваш аккаунт должен быть утвержден администратором вручную, прежде чем его можно будет использовать. + Пожалуйста, заполните все поля! + Пароли не совпадают! + Этот email недействителен! + Ваше имя пользователя будет уникальным на %1$s + Вам будет отправлено подтверждение по электронной почте + Используйте не менее 8 символов + Пароль должен содержать не менее 8 символов + Имя пользователя может содержать только буквы, цифры и символ подчеркивания + Аккаунт создан! + Ваша учетная запись создана!\n\n + Подтвердите вашу электронную почту в течение 48 часов.\n\n + Теперь вы можете подключить ваш аккаунт, написав %1$s в первом поле и кликнув Подключить.\n\n + Важно: Если ваш инстанс требует проверки, вы получите письмо после ее завершения! + + Сохранить сообщение в черновиках? + Администрирование + Отчеты + Нет отчетов для отображения! + Повторно подключить аккаунт + Приложение не смогло получить доступ к функциям администрирования. Возможно вам необходимо повторно подключить учетную запись, чтобы получить нужные права. + Нерешенный + Удаленный + Активный + В ожидании + Отключен + Приглушен + Приостановлен + Разрешения + Статус email + Статус логина + Вступивший + Самый последний IP + Предупреждение + Отключение + Приглушение + Уведомить пользователя по электронной почте + Предупреждение пользователя + Пользователь + Модератор + Администратор + Подтвержден + Не подтвержден + Зарегистрированные статусы + Аккаунт + Отменить приглушение + Отменить отключение + Приостановка + Отменить приостановление + Этот аккаунт приглушен! + Этот аккаунт больше не приглушен! + Этот аккаунт приостановлен! + Этот аккаунт больше не приостановлен! + Этот аккаунт отключен! + Этот аккаунт больше не отключен! + Этот аккаунт предупрежден! + Отображать меню администратора + Отображать функции администрирования в статусах + Разрешить + Учетная запись одобрена! + Учетная запись отклонена! + Назначить мне + Отменить назначение + Пометить как решенный + Пометить как нерешенный + Контента нет! + Показать кнопку функций Fedilab + Приложению требуется доступ к записи аудио + Голосовое сообщение + Включить быстрый ответ + Аккаунт, которому вы отвечаете, может не увидеть ваше сообщение! + Если отключено, приложение всегда будет загружать последние статусы + Если отключено, деликатный контент будет скрыт кнопкой + Сохранять мультимедиа в полном размере при длительном нажатии на превью + Добавить кнопку ⋮ в правом верхнем углу для отображения всех тегов /инстансов/списков + В течение этого промежутка времени приложение отправит вам уведомление. Вы можете изменить (отключить) этот временной интервал с помощью спиннера. + Включает кнопку Fedilab под изображением профиля для быстрого доступа к функциям. + Разрешить отвечать прямо в ленте ниже статусов + Превью не будут обрезаны в лентах + Разрешить воспроизведение встроенных видео прямо в ленте + Позволяет изменить способ чтения статусов, которые отображаются при нажатии кнопки \'Получить больше\' + Эта опция позволяет поддерживать последние наборы шифров. Это полезно для старых устройств Android или если вы не можете подключиться к вашему инстансу. + Только для видеороликов Peertube. Переключите, если они у вас не воспроизводятся. + Эти теги позволят отфильтровывать статусы из профилей. Вы должны будете использовать контекстное меню для их просмотра. + Автоматически вставлять разрыв строки после упоминания для ввода заглавной первой буквы + Разрешить создателям контента обмениваться статусами в своих RSS-каналах + Создание + Максимальное количество повторов при загрузке мультимедиа + Создать новую папку здесь + Введите название папки + Пожалуйста, введите правильное название папки + Эта папка уже существует.\n Пожалуйста, укажите другое имя для папки + Выбрать + Каталог по умолчанию + Папка + Создать папку + Отобразить уведомление после выполнения действия (продвижение, добавление в избранные и т. д.)? + Игнорируемые инстансы были экспортированы! + Добавить инстанс + Экспорт инстансов + Импорт инстансов + Отчеты о сбоях + Включить отчеты о сбоях + Если включено, отчет о сбое будет создан локально. Вы сможете поделиться им при необходимости. + Fedilab остановился :( + Вы можете отправить мне по электронной почте отчет о сбое. Это поможет его исправить :)\n\nВы можете добавить дополнительную информацию. Спасибо! + Использовать WYSIWYG + Если включено, вы сможете легко форматировать текст с помощью инструментов. + Статистика + Всего статусов + Количество продвижений + Количество избранных + Количество упоминаний + Количество подписчиков + Количество опросов + Количество ответов + Количество статусов + Статусы + Видимость + Количество с медиа + Количество с деликатным медиа + Количество с 18+ + Дата первого статуса + Дата последнего статуса + Дата первого уведомления + Дата последнего уведомления + Периодичность + %s статусов в день + %s уведомлений в день + Диапазон дат + Группы + Нет групп! + Отключить пользовательские анимированные эмодзи + Графики + Отображать графики + Приложение собирает ваши локальные данные, пожалуйста, подождите... + Резервное копирование + Статус автоматического резервного копирования + Эта опция для каждого аккаунта. Она запустит службу, которая автоматически сохранит ваши статусы в локальной базе данных. Это позволит получить статистику и графики + Автоматическое резервное копирование уведомлений + Эта опция для каждого аккаунта. Она запустит службу, которая автоматически сохранит ваши уведомления в локальной базе данных. Это позволит получить статистику и графики + Сообщить об аккаунте + Отправить приглашение + Ваш инстанс не разрешает зарегистрировать новый аккаунт! + + %d голос + %d голоса + %d голосов + %d голосов + + + %d голосующий + %d голосующих + %d голосующих + %d голосующих + + + Единичный выбор + Множественный выбор + + + 5 минут + 30 минут + 1 час + 6 часов + 1 день + 3 дня + 7 дней + + + Torrent + Webview + + Чтобы присоединиться к моему инстансу \"%1$s\", вам необходимо загрузить Fedilab:\n\nF-Droid: %2$s\nnGoogle: %3$s\n\nЗатем откройте ссылку ниже при помощи Fedilab и создайте свой аккаунт :)\n\n%4$s + + Ваш опрос не может иметь дублирующих друг друга вариантов! + Для всех аккаунтов + Кэш базы данных + Очистить кэш вашей домашней ленты + Очистить кэш ваших статусов + Очистить ваши закладки + Файлы в кэше + Всего уведомлений + Скрыть пункты меню + Fedilab запускает живые уведомления + Для %1$s аккаунтов с %2$s событиями + Живые уведомления для %1$s + Живые уведомления будут отключены только для этого аккаунта. + Очистить кеш при выходе + Кэш (медиа, кэшированные сообщения, данные из встроенного браузера) будут автоматически очищаться при выходе из приложения. + Вы хотите отписаться от этого аккаунта? + Предупреждать перед отменой подписки + Заменить Youtube на Invidio.us + Invidious - это альтернативный интерфейс для YouTube + Введите собственный хост или оставьте пустым для использования invidio.us + Заменить Твиттер на Nitter + Nitter - это альтернатива Twitter с открытым исходным кодом, ориентированная на конфиденциальность. + Введите собственный хост или оставьте пустым для использования nitter.net + Заменить Instagram на Bibliogram + Bibliogram это альтернативный front-end для Instagram с открытым исходным кодом и сфокусированный на приватности. + Введите свой собственный хост или оставьте пустым для использования bibliogram.art + Заменить Reddit на Libreddit + Libreddit - это альтернатива Reddit с открытым исходным кодом, ориентированная на конфиденциальность. + Введите свой собственный хост или оставьте поле пустым, чтобы использовать libredd.it + Replace Medium links + Replace medium.com links with an open source alternative front-end focused on privacy. + Default: scribe.rip + Replace Wikipedia links + Replace Wikipedia link with an open source alternative front-end focused on privacy. + Default: wikiless.org + Скрыть панель уведомлений Fedilab + Чтобы скрыть уведомление в строке состояния, нажмите на кнопку с пиктограммой глаза и снимите флажок: \"Показывать в строке состояния\" + Использовать систему push-уведомлений для получения уведомлений в режиме реального времени. + Нет живых уведомлений + Live notifications + Уведомления будут поступать каждые 15 минут. + Добавить примечания + Примечания для аккаунта + Позволяет сжимать большие фотографии в меньшие по размеру с очень маленькой или незначительной потерей качества изображения. + Разрешить сжатие видео с сохранением при этом его качества. + Приложение сжимает медиа, это может продолжаться достаточно долго… + Изменение значка приложения + Нажмите, чтобы изменить значок приложения + Опубликовать + Видимость записи + Нажмите здесь, чтобы добавить фотографии + Поддерживаемые форматы: jpeg, png, gif \n\nМаксимальный размер файла: 15 Мб \n\nАльбомы могут содержать 4 фотографии или видео + Загрузить медиафайл + Добавить подпись (необязательно) + Приложение получило очень длинное сообщение об ошибке от API %1$s + Предпросмотр сообщения + Добавлять упоминания в каждом сообщении + Загрузка диалога + Упорядочить по + Заголовок видео + Присоединиться к Peertube + Мне 16 или более лет и я согласен с %1$s этого инстанса + Ссылки + Изменить цвет ссылок (URL, обращения, теги и т.д.) в сообщениях + Заголовок продвижений + Изменить цвет отображаемого имени в верхней части сообщений + Изменить цвет имени пользователя в верхней части сообщений + Изменить цвет заголовка продвижений + Записи + Цвет фона записей в лентах + Сбросить цвета + Нажмите здесь, чтобы сбросить все ваши пользовательские цвета + Сброс + Иконки + Цвет нижних значков в лентах + Закрепить этот тег + Логотип инстанса + Редактировать профиль + Предпринять действие + Перевод + Предпросмотр изображения + Цвет текста + Изменить цвет текста в записях + Применить изменения + Вам понадобится перезапустить клиент, чтобы применить изменения + Перезапустить + Использовать собственную тему + Позволяет переопределить цвета выбранной темы выше + Настройки оформления + Сначала сохранить + Тема была экспортирована + Тема была успешна экспортирована в формат CSV + Применить основной цвет к строке состояния + Цвет строки состояния + Восстановить стандартную тему + Импортировать тему + Нажмите здесь чтобы импортировать тему из предыдущего экспорта + Экспортировать тему + Нажмите здесь для экспорта текущей темы + Произошла ошибка при выборе файла темы + Выбор темы + Выбрать предустановленную тему + Темы + Применить основной цвет к панели навигации + Цвет панели навигации + Базовый цвет содержимого приложения. + Цвет фона + Акценты выбирают части пользовательского интерфейса. + Цвет акцента + Отображаемые чаще всего в вашем приложении. + Основной цвет + Экспортировать закладки в инстанс + Импортировать закладки из инстанса + Количество пользователей + Количество статусов + Количество инстансов + Заблокировано + Заканчивается в %s + Что нового в %s + Вы можете подписаться на мой аккаунт чтобы следить за обновлениями + Этот инстанс не доступен на https://instances.social + Показать полную ссылку + Поделиться ссылкой + URL была скопирована в буфер обмена + Открыть с помощью другого приложения + Проверить перенаправление + Этот URL не перенаправляет + %1$s \n\nперенаправляет на\n\n %2$s + Изменить user agent + Задать свой user agent или оставить пустым + Позволяет изменить user agent используемый для api вызовов или во встроенном браузере. + Удалить UTM параметры + Приложение будет автоматически удалять UTM параметры из URL-адреса, перед посещением ссылки. + Актуальное + Актуальное сейчас + %d люди говорят + Аккаунты Twitter (с помощью Nitter) + Имена пользователей Twitter разделённые пробелом + Подтверждение личности + Подтверждённая личность + Подтверждено %1$s (%2$s) + Удалить уведомление + Показать больше опций + Это Pixelfed история + Загрузите медиафайл, он будет автоматически добавлен к вашей Pixelfed истории. + Медиафайл успешно добавлен в вашу историю! + Действие отключено + Отписаться + Что-то пошло не так, пожалуйста, проверьте папку загрузок в настройках. + Объявления + Объявлений пока нет. + Добавить реакцию + Используйте ваш любимый браузер внутри приложения. Снимите флажок на эту функцию, чтобы открывать ссылки внешним браузером. + Кэш видео в MB, ноль означает, нет кэша. + Водяные знаки + Автоматически добавлять водяной знак внизу изображений. Текст можно настроить для каждого аккаунта. + Дистрибьюторы не найдены! + Вам нужен дистрибьютор для получения push-уведомлений.\nВы найдете более подробную информацию по адресу %1$s.\n\nВы также можете отключить push-уведомления в настройках для игнорирования этого сообщения. + Выберите дистрибьютора + diff --git a/app/src/main/res/values-sc/strings.xml b/app/src/main/res/values-sc/strings.xml new file mode 100644 index 000000000..c76b150a0 --- /dev/null +++ b/app/src/main/res/values-sc/strings.xml @@ -0,0 +1,1105 @@ + + + Aberi su menù + Serra su menù + Informatziones + Informatziones subra s\'istàntzia + Riservadesa + Memòria temporànea + Essi + Intra + + Serra + Eja + Nono + Annulla + Iscàrriga + Iscàrriga %1$s + Cuntenutu multimediale sarvadu + Documentu: %1$s + Crae + Posta eletrònica + Contos + Tuts + Etichetas + Sarva + Riprìstina + Perunu resurtadu! + Istàntzia + Istàntzia: mastodon.social + Como funtzionat cun su contu %1$s + Agiunghe unu contu + As copiadu su cuntenutu de su tut in punta de billete + As copiadu s\'URL de su tut in punta de billete + Càmbia + Seletziona un\'immàgine… + Lìmpia + Fotocàmera + Iscantzella totu + Tradui custu tut. + Pranìfica + Mannària de su testu e de is iconas + Càmbia sa mannària atuale de su testu: + Càmbia sa mannària atuale de is iconas: + Sighi + Pretzedente + Aberi cun + Vàlida + Cuntenutu multimediale + Cumpartzi cun + Cumpartzidu impreende Fedilab + Rispostas + Nùmene utente + Abbotzos + Preferidos + Sighidores noos + Mentovos + Cumpartziduras + Ammustra is cumpartziduras + Ammustra rispostas + Aberi in su navigadore + Tradui + Iseta unos cantos segundos in antis de fàghere custa atzione. + + Printzipale + Lìnia de tempus locale + Lìnia de tempus federada + Optziones + Preferidos + Comunicatzione + Persones a sa muda + Persones blocadas + Notìficas + Rechestas de sighidura + Cunfiguratziones + Cantzella unu contu + Boles cantzellare su contu %1$s dae s\'aplicatzione? + Imbia una lìtera eletrònica + Toca in s\'andala pro la cambiare + Faddina! + Tuts pranificados + Is informatziones inoghe in suta diant pòdere rapresentare su profilu de s\'utente in manera incumpleta. + Inserta un\'emoji + S\'aplicatzione no at ancora collidu peruna emoji personalizada. + Notìficas push + Ses seguru de bòlere essire? + Ses seguru de bòlere essire @%1$s@%2$s? + + Perunu tut de ammustrare + Peruna istòria de ammustrare + Istòrias + Cumpatzidu dae %1$s + Boles agiùnghere custu tut a is preferidos tuos? + Boles bogare custu tut dae is preferidos tuos? + Boles cumpartzire custu tut? + Acabare de cumpartzire custu tut? + Boles apicare custu tut? + Boles bogare custu tut dae is apicados? + A sa muda + Bloca + Signala + Boga + Còpia + Cumpartzi + Mentova + Silentziamentu temporizadu + Cantzella e torra a iscrìere + + Pònnere a sa muda custu contu? + Blocare custu contu? + Sinnalare custu tut? + Blocare custu domìniu? + Bogare su silentziamentu a custu contu? + Isblocare custu contu? + + + Notìfica + A sa muda + + + Bogare custu tut? + Cantzellare e torrare a iscrìere custu tut? + + Sinnalibros + Agiunghe a is sinnalibros + Boga su sinnalibru + Perunu sinnalibru de ammustrare + S\'istadu est istadu agiuntu a is sinnalibros! + S\'istadu est istadu bogadu dae is sinnalibros! + + %d s + %d m + %d o + %d d + + %d segundu + %d segundos + + + %d minutu + %d minutos + + + %d ora + %d oras + + + %d die + %d dies + + + Dae cara + A ite ses pensende? + TUT! + CUITA! + ac + Iscrie unu tut + Risponde a unu tut + Iscrie unu cuit + Risponde a unu cuit + Seletziona unu mèdiu + Ddoe est istadu un\'errore seletzionende su cuntenutu multimediale! + Cantzellare custu mèdiu? + Su tut tuo est bòidu! + Visibilidade de su tut + Visibilidade predefinida de is tuts: + Su tut est istadu imbiadu! + Ses rispondende a custu tut: + Cuntenutu sensìbile? + + Pùblica in is lìnias de tempus pùblicas + Non pùbliches in is lìnias de tempus pùblicas + Pùblica sceti cara a is sighidores + Pùblica sceti cara a is utentes mentovados + + Perunu abbotzu! + Sèbera unu tut + Sèbera unu contu + Seletziona unos cantos contos + Boles cantzellare s\'abbotzu? + Toca in su butone pro ammustrare su tut originale + Decrie·lu pro chie tenet problemas de visione + + Peruna descritzione a disponimentu! + + Versione %1$s + Isvilupadore: + Litzèntzia: + GNU GPL V3 + Còdighe mitza: + Tradutzione de is tuts: + Istàntzias de chirca: + Disinnadore de is iconas: + + Arresonada + + Perunu contu de ammustrare + Peruna rechesta de sighidura + Tuts \n %1$s + Sighende \n %1$s + Sighidores \n %1$s + Apicadu \n %d + Autoriza + Refuda + + Perunu tut pranificadu de ammustrare! + Iscrie unu tut e sèbera <b >Pranìfica</b> dae su menù in artu. + Cantzellare su tut pranificadu? + Mèdiu: %d + Su tut est istadu pranificadu! + Su data de pranìfica depet èssere a pustis de como! + Su rispàrmiu energèticu est abilitadu! Diat pòdere non funtzionare comente si tocat. + + Su tempus de silentziamentu depet èssere prus mannu de unu minutu. + %1$s est istadu postu a sa muda finas a su %2$s.\n Podes bogare su silentziamentu a custu contu dae sa pàgina de profilu sua. + %1$s est postu a sa muda finas a su %2$s.\n Toca inoghe pro bogare su silentziamentu a su contu. + + Peruna notìfica de ammustrare + t\'at mentovadu + at iscritu unu messàgiu nou + at cumpartzidu s\'istadu tuo + at agiuntu s\'istadu tuo a is preferidos suos + at comintzadu a ti sighire + at pedidu de ti pòdere sighire + + e un\'àtera notìfica + e àteras %d notìficas + + + %d agradessimentu + %d agradessimentos + + Cantzellare una notìfica? + Cantzellare totu is notìficas? + Sa notìfica est istada cantzellada! + Totu is notìficas sunt istadas cantzelladas! + + Sighende + Sighidores + Apicadu + + Impossìbile otènnere s\'id de su cliente! + Connessione a su domìniu de s\'istàntzia fallida! + Peruna connessione a ìnternet! + Su contu est istadu blocadu! + Su contu no est prus blocadu! + Su contu est istadu postu a sa muda! + Su contu no est prus a sa muda! + Su contu est sighidu! + Non sighis prus su contu! + Su tut est istadu cumpartzidu! + Su tut no est prus cumpartzidu! + As agiuntu su tut a is preferidos tuos! + Su tut est istadu bogadu dae is preferidos tuos! + Su tut est istadu sinnaladu! + Su tut est istadu cantzelladu! + Su tut est istadu apicadu! + Su tut no est prus apicadu! + Oops! Ddoe est istadu un\'errore! + Ddoe est istadu un\'errore! S\'istàntzia no at frunidu unu còdighe de autorizatzione! + Custu domìniu de istàntzia non paret èssere vàlidu! + Ddoe est istadu un\'errore colende dae unu contu a s\'àteru! + Ddoe est istadu un\'errore cun sa chirca! + Is datos de su profilu sunt istados sarvados! + Non podes fàghere peruna atzione + Su mèdiu est istadu sarvadu! + Ddoe est istadu un\'errore cun sa tradutzione! + Is tradutziones sunt disabilitadas in is cunfiguratziones + Abbotzu sarvadu! + Ses seguru chi cust\'istàntzia permitat custu nùmeru de caràteres? Pro su prus custu valore est a curtzu a 500 caràteres. + Sa visibilidade de is tuts est istada cambiada pro su contu %1$s + + Nùmeru de tuts pro carrigamentu + Semper + WIFI + Pedi + Càrriga is cuntenutos multimediales + Càrriga is immàgines + Ammustra de prus… + Ammustra de mancu… + Cuntenutu sensìbile + Disabìlita is avatars GIF + Àndala: + Sarva is abbotzos automaticamente + Agiunghe is URL de sos cuntenutos multimediales in is tuts + Notìfica cando calicunu ti sighet + Notìfica cando calicunu cumpartzit s\'istadu tuo + Notìfica cando calicunu ponet s\'istadu tuo in is preferidos suos + Notìfica cando calicunu ti mentovat + Notìfica cando acabat unu sondàgiu + Notìfica pro sas publicatziones noas + Ammustra una ventanedda de diàlogu de cunfirma in antis de cumpartzire + Ammustra una ventanedda de diàlogu de cunfirma in antis de agiùnghere a is preferidos + Notìfica sceti cun su WIFI + Notificare? + Notìficas a sa muda + Tempus de isetu pro sa visualizatzione de materiale sensìbile NSFW (segundos, 0 cheret nàrrere istudadu) + Tempus de isetu de sa descritzione de is cuntenutos multimediales (segundos, 0 bolet nàrrere istudadu) + Modìfica su profilu + Cumpartzidura personalizada + S\'URL de sa cumpartzidura personalizada tua… + Biografia… + Imposta su contu privadu + Sarva is modìficas + Sèbera un\'immàgine de intestatzione + Adata is antiprimas de is immàgines + Partzi automaticamente is tuts in rispostas cando is caràteres sunt prus de: + Ses arribbadu a is 160 caràteres permìtidos! + Ses arribbadu a is 30 caràteres permìtidos! + Intre is + e is + S\'oràriu depet èssere prus mannu de %1$s + S\'oràriu depet èssere prus piticu de %1$s + Ora de cumintzu + Ora de acabu + Imprea su navigadore integradu + Ischedas personalizadas + Abìlita Javascript + Ismànnia automaticamente is ac + Permite is testimòngios de tertzas partes + Sa crae API tua. La podes lassare bòida pro Yandex + + Iscuru + Craru + Nieddu + + Imposta su colore LED: + + Biaitu + Cianu + Magenta + Birde + Ruju + Grogu + Biancu + + Sighi + Isbloca + A sa muda + Ativa su sonu + Pregunta imbiada + Ti sighit + Chirca + Prima lìtera majùscula in is rispostas + Càmbia sa mannària de is immàgines + Càmbia sa mannària de is vìdeos + + Notìficas push + Pro praghere cunfirma is notìficas chi cheres retzire. As a pòdere abilitare o disabilitare custas notìficas prus a tardu in is cunfiguratziones (barra de is notìficas). + + Isbòida sa memòria temporànea + Tenes %1$s de datos in sa memòria temporànea tua.\n \n Ddos dias bòlere cantzellare? + Mb + Sa memòria temporànea est istada isboidada! Si sunt liberados %1$s + + Tìtulu + Tìtulu… + Descritzione + Paràulas crae + Paràulas crae… + + Sincroniza + Filtru + Is tuts tuos + Is notìficas tuas + Pùblicu + Esclùidu de sa lista + Privadu + Deretu + Unas cantas paràulas crae… + Ammustra is cuntenutos multimediales + Ammustra is apicados + Perunu resurtadu chi currispondat agatadu! + Faghe una còpia de seguresa de is tuts de %1$s + As importadu %1$s tuts noos + As importadu %1$s notìficas noas + + Datas menguantes + Datas in artziada + + + Nono + Ebbia + Ambos + + Perunu tut agatadu in sa base de datos. Imprea su butone de sincronizatzione dae su menù pro ddos recuperare. + + Datos registrados + In su dispositivu benint sarvadas sceti informatziones debase de sos contos. Custas informatziones sunt cunfidentziales e ddas podet impreare s\'aplicatzione ebbia. Cantzellende s\'aplicatzione as a cantzellare immediatamente custos datos.\n ⚠ Is datos de intrada e is craes de intrada non benint sarvados mai. Benint impreados sceti durante un autenticatzione segura (SSL) cun un\'istàntzia. + Permissos: + - <b >ACCESS_NETWORK_STATE</b>: Impreadu pro rilevare si su dispositivu est connessu a una retza WIFI.\n - <b >INTERNET</b>: Impreadu pro consultare is istàntzias.\n - <b >WRITE_EXTERNAL_STORAGE</b>: Impreadu pro sarvare documentos multimediales o tramudare s\'aplicatzione a un\'ischeda SD.\n - <b >READ_EXTERNAL_STORAGE</b>: Impreadu pro agiùnghere documentos multimediales a is tuts.\n - <b >BOOT_COMPLETED</b>: Impreadu pro allùghere su servìtziu de notìfica.\n - <b >WAKE_LOCK</b>: Impreadu durante su servìtziu de notìfica. + Permissos de s\'API: + - <b >Leghe</b>: Leghe datos.\n - <b >Iscrie</b>: Pùblica istados e càrriga cuntenutos multimediales pro is istados.\n - <b >Sighi</b>: Sighi, acaba de sighire, bloca, isbloca.\n \n <b >⚠ Custas atziones benint fatas sceti cando lu pedit s\'utente.</b> + + Arrastamentu e librerias + S\'aplicatzione <b >no impreat ainas de arrastamentu</b> (medida de su pùblicu, sinnalatzione de errores, etc.) e non cuntenet publitzidade peruna.\n \n S\'impreu de librerias est minimadu: \n - <b >Glide</b>: Pro gestire is cuntenutos multimediales (\"mèdios\")\n - <b >Android-Job</b>: Pro gestire is servìtzios\n - <b >PhotoView</b>: Pro gestire is immàgines + Tradutzione de is tuts + Custa aplicatzione frunit sa possibilidade de bortare is tuts impreende sa limba de su dispositivu e s\'API de Yandex.\n Yandex tenet una polìtica pro sa riservadesa sua, chi podes agatare inoghe: https://yandex.ru/legal/confidential/?lang=en + Torramus gràtzias a: + Filtra cun espressiones regulares + Chirca + Cantzella + Recùpera àteros tuts… + + Listas + Seguru chi boles cantzellare custa lista in manera permanente? + Non ddoe est ancora nudda in custa lista. Cando is persones de custa lista ant a publicare àteros istados, ant a aparèssere inoghe. + Agiùnghe a sa lista + Agiunghe lista + Cantzella sa lista + Modìfica sa lista + Lista noa + Contu annantu a sa lista + Non tenes ancora peruna lista! + + %1$s s\'est tramudadu a %2$s + S\'autenticatzione no est funtzionende? + <b >Custas sunt una paia de verìficas chi diant pòdere agiudare:</b>\n \n - Verìfica chi non bi siant faddinas de iscritura in su nùmene de s\'istantzia\n \n - Verìfica chi s\'istàntzia tua non siat inativa\n \n - Si impreas s\'autenticatzione in duas fases (A2F/2FA), imprea su ligàmene in fundu (a pustis de àere insertadu su nùmene de s\'istàntzia)\n \n - Podes fintzas impreare custu ligàmene sena impreare sa 2FA\n \n - Si sighet a non funtzionare, aberi una sinnalatzione in FramaGit in https://framagit.org/tom79/fedilab/issues + Su cuntenutu multimediale est istadu carrigadu. Toca inoghe pro dd\'ammustrare. + Custa atzione diat pòdere durare a longu meda. As a retzire una notìfica cando at a èssere acabada. + Galu traballende, iseta pro praghere… + Esporta is istados + Esporta is istados pro %1$s + %1$s de %2$s tuts sunt istados esportados. + B\'at àpidu unu problema esportende is datos pro %1$s + B\'at àpidu unu problema esportende is datos! + B\'at àpidu carchi problema cun s\'importatzione de is datos! + + Server intermediàriu + Abilitare su server intermèdiu? + Istrangiadore (host) + Ghenna + Intra + Crae + Agiunghe detàllios a is tuts cando los ses cumpartzende + Suporta s\'aplicatzione in Liberapay + Dd\'est una faddina in s\'espressione regulare! + Peruna lìnia de tempus agatada in custa istàntzia! + Cantzellare cust\'istàntzia? + Borta in + Sighi s\'istàntzia + Ses giai sighende custa istàntzia! + S\'istàntzia est sighida! + Collaboratziones + Informatziones + Cua is cumpartziduras de %s + Cussìgia in su profilu tuo + Ammustra is cumpartziduras dae %s + Non cussiges in su profilu + Su contu como est in evidèntzia in su profilu tuo + Su contu no est prus postu in evidèntzia in su profilu + Is cumpartziduras como benint ammustradas! + Is cumpartziduras como sunt cuadas! + Messàgiu diretu + Filtros + Perunu filtru de ammustrare. Nde podes creare unu incarchende in su butone \"+\". + Paràula o fràsia crae + Lìnia de tempus printzipale + Lìnias de tempus pùblicas + Notìficas + Arresonadas + Su cunfrontu s\'at fàghere ignorende is majùsculas e sas minùsculas e is avisos de cuntenutu de unu tut + Ignora imbetzes de cuare + Is tuts filtrados ant a isparèssere in manera irreversìbile, fintzas si a pustis as a bogare su filtru + Cando sa paràula o sa fràsia crae est alfanumèrica ebbia, at a èssere aplicada petzi si currispondet a sa paràula intrea + Totu su mundu + Filtra is cuntestos + Unu o prus cuntestos in ue su filtru si diat dèpere aplicare + Iscadit a pustis de + Cantzellare su filtru? + Agiorna su filtru + Crea unu filtru + Chie sighire + In custu momentu non dd\'est perunu contu allistadu! + Sighi + Seletziona totu + Annulla sa seletzione + Sighis a %s! + Creende sa lista %s + Agiunghende contos a sa lista + Is contos sunt istados agiuntos a sa lista + Agiunghende contos a sa lista + No as creadu ancora peruna lista. Toca in su butone \"+\" pro nde agiùnghere una noa. + Chie sighire + API Trunk + Non podes sighire su(is) contu(os) + Recuperende su contu remotu + Ismànnia automaticamente sos cuntenutos cuados + Sighidura noa + Cumpartzidura noa + Preferidu nou + Mentovu nou + Sondàgiu acabadu + Tut nou + Còpia de seguresa de is tuts + Publicatziones noas + Iscàrriga is mèdios + Càmbia su sonu de notìfica + Seletziona sa soneria + Abìlita sa fàscia orària + Vìdeos de agiudu + Recuperende s\'arresonada remota! + Perunu domìniu blocadu! + Isbloca su domìniu + Ses seguru de bòlere isblocare %s? + Ses seguru de bòlere blocare %s?\n \n No as a bìdere perunu cuntenutu dae custu domìniu o in is notìficas tuas. Is sighidores tuos dae custu domìniu ant a èssere bogados. + Domìnios blocados + Bloca su domìniu + Su domìniu est blocadu + Su domìniu no est prus blocadu! + Recuperende s\'istadu remotu + Cummenta + Istàntzia de Peertube + Sias su primu a lassare unu cummentu a custu vìdeu cun su butone in artu a destra! + %s visualizatziones + Longària: %s + Agiunghe un\'istàntzia + Is cummentos non sunt abilitados in custu vìdeu! + Sèbera una risolutzione + Preferidos de Peertube + As agiuntu su vìdeu a is sinnalibros! + As bogadu su vìdeu dae is sinnalibros! + Non tenes perunu vìdeu de Peertube in is preferidos tuos! + Canale + Vìdeos + Canales + Imprea is Emoji One + Informatziones + Ammustra is antiprimas in totu is tuts + Disinnadore UX/UI nou + Ammustra is antiprimas de is vìdeos + S\'id de su contu est istadu copiadu in punta de billete + Càmbia sa limba + Limba predefinida + Trunca is tuts longos + Trunca is tuts prus longos de \'x\' lìnias. Zero cheret nàrrere disabilitadu. + Ammustra àteru + Ammustra de mancu + Amministra is etichetas + S\'eticheta esistit giai! + S\'eticheta est istada sarvada! + S\'eticheta est istada cambiada! + S\'eticheta est istada cantzellada! + Pranìfica una cumpartzidura + As pranificadu sa cumpartzidura! + Peruna cumpartzidura pranificada de ammustrare! + Pranìfica sa cumpartzidura.]]> + Lìnia de tempus de Arte + Aberi su menù + Torra in segus + Logotipu de s\'aplicatzione + Immàgine de su profilu + Immàgine de profilu + Cuntata s\'amministradore de s\'istàntzia + Agiunghe nou + Logotipu de MastoHost + Seletzionadore de emoji + Atualiza + Ismànnia s\'arresonada + Boga unu contu + Cantzella su domìniu blocadu + Seletzionadore de emoji personalizadas + Riprodui su vìdeu + Tut nou + Immàgine de s\'ischeda + Cua mèdia + Icona de is preferidos + Agiunghe una descritzione pro su cuntenutu multimediale (pro chie tenet problemas de bisione) + + Mai + 30 minutos + 1 ora + 6 oras + 12 oras + 1 die + 1 chida + + In custu campu depes iscrìere su domìniu de s\'istàntzia tua.\n A esèmpiu, si as creadu su contu tuo in https://mastodon.social\n Iscrie sceti <b >mastodon.social</b>\n Podes cumintzare a iscrìere is primas lìteras e as a retzire cussìgios pro is nùmenes. + Àteras informatziones + + Limbas + Multimediale isceti + Ammustra is cuntenutos sensìbiles (NSFW) + Tradutziones de Crowdin + Amministradore de Crowdin + Tradutzione de s\'aplicatzione + A pitzu de Crowdin + Bot + Istàntzia Pixelfed + Istàntzia de Mastodon + Cale si siat de custos + Totu custos + Perunu de custos + Una cale si siat intre custas paràulas (separadas dae ispàtzios) + Totu custas paràulas (separadas dae ispàtzios) + Agiunghe unas cantas paràulas a su filtru (separadas dae ispàtzios) + Càmbia su nùmene de sa colonna + Istàntzia Misskey + Non tenes aplicatzione peruna chi suportet custu ligàmene installada in su dispositivu tuo. + Abbonamentos + Resumu + De tendèntzia + Agiuntos dae pagu + Locale + Càrriga + Risponde + Cantzella unu cummentu + Ses seguru de bòlere cantzellare custu cummentu? + Vìdeu a ischermu intreu + Modalidade pro is vìdeos + Seletziona su documentu de carrigare + Vìdeos meos + Tìtulu + Litzèntzia + Categoria + Limba + Custu vìdeu tenet cuntenutos pro adultos o esplìtzitu + Abìlita is commentos a su vìdeu + Agiorna su vìdeu + Descritzione + Su vìdeu est istadu agiornadu! + Carrigamentu annulladu! + Su vìdeu est istadu carrigadu! + Carrighende, iseta… + Toca inoghe pro modificare is datos de su vìdeu. + Cantzella su vìdeu + Ses seguru de bòlere cantzellare custu vìdeu? + Ammustra is vìdeos non seguros pro su traballu (NSFW) + Perunu vìdeu de ammustrare! + Lassa unu cummentu + Cumpartzi + Sèbera una modalidade de pranificatzione + Dae su dispositivu + Dae su serbidore + Tuts (Serbidore) + Tuts (Dispositivu) + Modìfica + Ammustra is tuts noos a pitzu de su butone \"Càstia àteru\" + Lìnias de tempus + Interfache + Cuntatos + %1$s at cummentadu su vìdeu tuo %2$s]]> + %1$s est sighende su canale tuo %2$s]]> + %1$s est sighende su contu tuo]]> + %1$s est istadu publicadu]]> + %1$s est acabada sena problemas]]> + %1$s est fallida]]> + %1$s at publicadu unu vìdeu nou: %2$s]]> + %1$s est istadu postu in sa lista niedda]]> + %1$s est istadu bogadu dae sa lista niedda]]> + Esporta is datos + Importa datos + Seletziona su documentu de importare + Ddoe est istadu un\'errore cun sa seletzione de su documentu de sa còpia de seguresa! + Agiunghe unu cummentu pùblicu + Imbia unu cummentu + Non ddoe est peruna connessione a ìnternet. Su messàgiu tuo est istadu sarvadu in is abbotzos. + Testu puru + HTML + Markdown + Essi dae su contu + Totu + Suporta s\'aplicatzione + Open Collective permitit a is grupos de impostare in presse unu colletivu, collire dinare e de dd\'amministrare in manera trasparente. + Còpia ligàmene + Connete + Normale + Cumpatadu + Terminale + Imposta sa modalidade de visualizatzione + Règula su frunidore de seguresa + Agiorna is domìnios de arrastamentu + Sa base de datos de arrastamentu est istada agiornada! + cramadas http blocadas dae s\'aplicatzione + Lista de mutidas blocadas + Imbia + As esportadu sa base de datos! + Etichetas in evidèntzia + Filtra sa lìnia de su tempus cun is etichetas + Peruna eticheta + Cua su butone \"iscantzella\" in s\'ischeda de sas notìficas + Allòngia un\'immàgine cando ses cumpartzende un\'URL + + Sondàgiu + Sondàgios + Crea unu sondàgiu + Sèberu 1 + Sèberu 2 + Sèberu %d + Tenes bisòngiu de a su mancu duos sèberos pro su sondàgiu! + Fatu + acaba in %s + Annoa su sondàgiu + Vota + Unu sondàgiu in su chi as votadu est acabbadu + Unu sondàgiu chi as publicadu est acabadu + Personaliza + Categorias + Fàscia orària + Avantzadas + Ammustra su distintivu \'nou\' in is tuts non lèghidos + Peertube + Move sa lìnia de tempus + Cua sa lìnia de su tempus + Torra a ordinare is lìnias de su tempus + Lista cantzellada definitivamente + Istàntzia sighida bogada + Eticheta apicada bogada + Annulla + Depes mantènnere duas ischedas visìbiles! + Torra a ordinare is lìnias de su tempus + Is lìnias de tempus printzipales si podent sceti cuare! + BBCode + Sinna semper is cuntenutos multimediales comente sensìbiles + Istàntzia GNU + Istadu in sa memòria temporànea + Inoltra is etichetas in is rispostas + Incarca a longu pro sarvare is cuntenutos multimediales + Isfoca is cuntenutos multimediales sensìbiles + Ammustra is lìnias de tempus in una lista + Ammustra is lìnias de su tempus + Marca is contos de bots in is tuts + Amministra is etichetas + Ammenta sa positzione in sa lìnia de tempus printzipale + Cronologia + Iscalitas + Nòmine visìbile + Non tenes peruna iscalita. Toca in su butone \"+\" pro agiùnghere un\'iscalita noa. + Depes frunire unu nùmene de ammustrare! + B\'at bisòngiu de su canale cando s\'iscalita est pùblica. + Crea un\'iscalita + In custa iscalita non dd\'est ancora nudda. + ripite + Galleria + Emoji + Adesivu + Gomma pro cantzellare + Testu + Filtru + Pintzellu + Ses seguru de bòlere essire sena sarvare s\'immàgine? + Iscarta + Sarvende... + Immàgine sarvada sena problemas! + Sarvamentu de s\'immàgine fallidu + Annapadura + Abìlita s\'editore de fotografias + Agiunghe un\'elementu a su sondàgiu + Boga s\'ùrtimu elementu de su sondàgiu + Silèntzia s\'arresonada + Boga su silentziamentu a s\'arresonada + S\'arresonada no est prus a sa muda! + S\'arresonada est impostada a sa muda + Aberi is funtzionalidades de s\'aplicatzione + Silentziamentu temporizadu + Mentova su contu + Annoa sa memòria temporànea + Mentova s\'istadu + Noas + Generales + Regionales + Arte + Giornalismu + Ativismu + Giogos + Tecnologia + Cuntenutu pro adultos + Furry + Màndigu + Logotipu de s\'istàntzia + Ddoe est istadu carchi problema chirchende is istàntzias a disponimentu! + Iscrie·ti a Mastodon + Sèbera un\'istàntzia pighende una categoria e, a pustis, tochende unu butone de verìfica. + Sèbera un\'istàntzia tochende unu butone de verìfica. + %1$s utentes + Cunfirma sa crae de intrada + Atzeto is %1$s e is %2$s + règulas de su serbidore + cunditziones de su servìtziu + Iscrie·ti + Cust\'istàntzia funtzionat cun invitos. Su contu tuo at a bisongiare de èssere aprovadu dae un\'amministradore in antis chi tue lu potzas impreare. + Iscrie in totu is campos, pro praghere! + Is craes non currispondent! + S\'indiritzu de posta non paret vàlidu! + Su nùmene de utente tuo at a èssere ùnicu in %1$s + As a retzire una lìtera eletrònica de cunfirma + Imprea a su mancu 8 caràteres + Sa crae de intrada diat cuntènnere a su mancu 8 caràteres + Su nùmene de s\'utente diat dèpere cuntènnere sceti lìteras, nùmeros e tratigheddos bassos + Contu creadu! + As creadu su contu tuo!\n \n Chirca de verificare s\'indiritzu de posta eletrònica tuo intre 48 oras.\n \n Como podes collegare su contu tuo iscriende <b >%1$s</b> in su de unu campu e incarchende in <b >Connete·ti</b>.\n \n <b >Cosa de importu</b>: Si s\'istàntzia tua tenet bisòngiu de èssere validada as a retzire una lìtera de posta eletrònica a pustis de sa validatzione! + Sarvare su messàgiu in is abbotzos? + Amministratzione + Sinnalatziones + Perunu raportu de ammustrare! + Torra a connètere su contu + S\'aplicatzione no est resèssida a atzèdere a is funtzionalidades de amministratzione. Dias pòdere dèpere torrare a collegare su contu pro tènnere is autorizatziones curretas. + Non risoltu + Dae remotu + Ativas + Isetende + Disabilitadu + A sa muda + Suspesu + Permissos + Istadu de sa posta eletrònica + Istadu de atzessu + Iscritu + IP prus reghente + Avisa + Disabìlita + Pone a sa muda + Notìfica s\'utente pro mèdiu de posta eletrònica + Avisu personalizadu + Utente + Moderadore + Amministradore + Cunfirmadu + Non cunfirmadu + Istados sinnalados + Contu + Annulla su silentziamentu + Annulla sa disabilitatzione + Suspende + Annulla sa suspensione + Su contu est silentziadu! + Su contu no est prus silentziadu! + Su contu est suspesu! + Su contu no est prus sospesu! + Su contu est disabilitadu! + Su contu no est prus disabilitadu! + Su contu est istadu avisadu! + Ammustra su menù de amministradore + Ammustra sa funtzionalidade de amministradore in is istados + Permite + Su contu est aprovadu! + Su contu est respintu! + Assigna·mi·lu + Annulla s\'assinnamentu + Sinna comente risoltu + Sinna comente non risoltu + Cuntenutu bòidu! + Ammustra su butone de sas funtzionalidades de Fedilab + S\'aplicatzione tenet bisòngiu de atzèdere a sa registratzione àudio + Messàgiu vocale + Abìlita is rispostas lestras + Su contu a su cale ses rispondende diat pòdere non bìdere su messàgiu tuo! + Si est disabilitadu s\'aplicatzione at a carrigare semper is ùrtimos istados + Si est disabilitadu sos cuntenutos multimediales ant a èssere cuados cun unu butone + Sarva is cuntenutos multimediales in mannària intrea incarchende a longu in is antiprimas + Agiunghe unu butone ellìticu in artu a destra pro allistare totu is etichetas/istàntzias/listas + Durante sa fàscia orària s\'aplicatzione at a imbiare notìficas. Podes furriare (pònnere a sa muda) custa fàscia orària cun su menù a destra + Ammustra unu butone de Fedilab in suta de s\'immàgine de profilu. Est un\'incurtzada pro atzèdere a is funtzionalidades de s\'aplicatzione. + Permite de rispòndere diretamente in is lìnias de tempus, dae suta de is istados + Is anteprimas no ant a èssere ritalliadas in is lìnias de tempus + Permite de riproduire vìdeos integrados diretamente in is lìnias de tempus + Permite de furriare sa manera de lèghere is istados chi benint ammustrados prus de una borta tochende su butone pro nde recuperare àteros + Custa atzione permitet de suportare is mètodos de tzifradura prus noos. Est discansosa pro dispositivos Android betzos o si non ti podes connètere a s\'istàntzia tua. + Pro is vìdeos de Peertube ebbia. Cola a custa modalidade si non resessis a los riproduire. + Custas etichetas t\'ant a permìtere de filtrare istados dae is profilos. As a depere impreare su menù de cuntestu pro ddos bìdere. + Inserta automaticamente un\'interrutzione de lìnia a pustis de su mentovu pro pònnere sa prima lìtera majùscula + Permite a is creadores de cuntenutos de cumpartzire is istados in is fontes de cuntenutos RSS issoro + Cumpone + Nùmeru màssimu de tentativos pro su carrigamentu de sos cuntenutos multimediales + Crea una cartella noa inoghe + Inserta su nùmene pro sa cartella + Inserta unu nùmene vàlidu pro sa cartella + Custa cartella esistit giai.\n Sèbera un\'àteru nùmene pro sa cartella + Seletziona + Cartella predefinida + Cartella + Crea una cartella + Boles ammustrare una ventanedda de messàgiu a pustis chi un\'atzione benit acabada (cumpatzidura, preferidu etz.)? + As esportadu is istàntzias postas a sa muda! + Agiunghe un\'istàntzia + Esporta is istàntzias + Importa istàntzias + Sinnalatziones de arrestu anòmalu + Abìlita is sinnalatziones pro is arrestos anòmalos + Si est abilitadu s\'at a creare una sinnalatzione in locale e l\'as a pòdere cumpartzire. + Fedilab s\'est firmadu :( + Mi podes imbiare su raportu de arrestu anòmalu pro mèdiu de posta eletrònica. At a agiudare a lu risòlvere :)\n\nPodes agiùnghere cuntenutos in prus. \"Gràtzias! + Imprea su wysiwyg (editore visuale) + Cando est abilitadu as a pòdere formatare su testu tuo in manera fàtzile cun is ainas. + Istatìsticas + Istados totales + Nùmeru de cumpartziduras + Nùmeru de preferidos + Nùmeru de mentovos + Nùmeru de sighiduras + Nùmeru de sondàgios + Nùmeru de rispostas + Nùmeru de istados + Istados + Visibilidade + Nùmeru cun cuntenutos multimediales + Nùmeru cun cuntenutos sensìbiles + Nùmeru cun AC + Data de su primu istadu + Data de s\'ùrtimu istadu + Data de sa prima notìfica + Data de s\'ùrtima notìfica + Frecuèntzia + %s istados a sa die + %s notìficas a sa die + Intervallu de datas + Grupos + Grupu perunu! + Disabìlita is emoji animadas personalizadas + Gràficos + Ammustra is gràficos + S\'aplicatzione est collende is datos locales tuos, iseta… + Còpia de seguresa + Faghe automaticamente una còpia de seguresa de is istados + Custa optzione est pro contu. At a aviare unu servìtziu chi at a archiviare in manera automàtica is istados tuos in sa base de datos. Custu permitet de otènnere istatìsticas e gràficos + Archìvia automaticamente is notìficas + Custa optzione est pro contu. At a aviare unu servìtziu chi at a archiviare in manera automàtica is notìficas tuas in sa base de datos. Custu permitet de otènnere istatìsticas e gràficos + Sinnala su contu + Imbia un\'invitu + S\'istàntzia tua non permitit de registrare unu contu nou! + + %d votu + %d votos + + + %d votante + %d votantes + + + Sèberos mùltiplos + Sèberos mùltiplos + + + 5 minutos + 30 minutos + 1 ora + 6 oras + 1 die + 3 dies + 7 dies + + + Vista web + Flussu diretu + + Pro t\'iscrìere a s\'istàntzia mea \"%1$s\", podes iscarrigare Fedilab:\n \n F-Droid: %2$s\n Google: %3$s\n \n A pustis aberi su ligàmene inoghe in suta cun Fedilab e crea su contu tuo :)\n \n %4$s + Su sondàgiu tuo non podet tènnere optziones duplicadas! + Pro totu is contos + Memòria temporànea de sa base de datos + Isbòida sa memòria temporànea de sa lìnia de tempus printzipale tua + Cantzella is istados sarvados in sa memòria temporànea + Isbòida is sinnalibros tuos + Documentos in sa memòria temporànea + Notìficas totales + Cua is elementos de su menù + Fedilab est faghende notìficas in tempus reale + Pro %1$s contos cun %2$s eventos + Notìficas in direta pro 1$s + Is notìficas in direta ant a bènnere abilitadas pro custu contu. + Isbòida sa memòria temporànea cando essis + Sa memòria temporànea (cuntenutos multimediales, messàgios sarvados in cue, datos de su navigadore integradu) s\'at a isboidare automaticamente cando as a essire dae s\'aplicatzione. + Boles acabare de sighire custu contu? + Ammustra una ventanedda de diàlogu de cunfirma in antis de acabare de sighire + Remplasa YouTube cun Invidio.us + Invidious est un\'interfache alternativa pro YouTube + Inserta s\'istrangiadore (host) personalizadu tuo o lassa bòidu pro invidious.snopyta.org + Remplasa Twitter cun Nitter + Nitter est un\'interfache alternativa a mitza aberta progetada pro sa riservadesa. + Inserta s\'istrangiadore (host) personalizadu tuo o lassa bòidu pro impreare nitter.net + Remplasa Instagram cun Bibliogram + Bibliogram est un\'interfache alternativa pro Instagram, a mitza aberta e progetada pro sa riservadesa. + Inserta s\'istrangiadore (host) personalizadu tuo o lassa bòidu pro bibliogram.art + Remplasa Reddit cun Libreddit + Libreddit est un\'interfache alternativa a mitza aberta pro Reddit progetada pro sa riservadesa. + Inserta s\'istrangiadore (host) personalizadu tuo o lassa bòidu pro impreare libredd.it + Remplasa is ligàmenes de Medium + Remplasa is ligàmenes de medium.com cun un\'interfache alternativa a còdighe abertu chi dat cara a sa riservadesa. + Predefinidu: scribe.rip + Remplasa is ligàmenes de Wikipedia + Remplasa is ligàmenes de Wikipedia cun un\'interfache alternativa a còdighe abertu chi dat cara a sa riservadesa. + Predefinidu: wikiless.org + Cua sa barra de is notìficas de Fedilab + Pro cuare is notìficas chi abarrant in sa barra de istadu toca in su butone cun s\'icona a forma de ogru e boga sa seletzione a: \"Ammustra in sa barra de istadu\" + Imprea unu sistema de notìficas push pro retzire notìficas in tempus reale. + Peruna notìfica in direta + Notìficas in direta + Sas notìficas ant a èssere recuperadas cada 15 minutos. + Agiunghe notas + Notas pro su contu + Permite de cumprimire is fotografias mannas in fotografias prus minores cun una pèrdida de calidade de s\'immàgine mìnima o irrilevante. + Permitet de cumprimire is vìdeos mantenende sa calidade issoro. + S\'aplicatzione est cumprimende su cuntenutu multimediale. Bi diat podere bòlere tempus meda… + Càmbia s\'icona de s\'aplicatzione + Toca pro cambiare s\'icona de s\'aplicatzione + Publicatzione + Visibilidade de sa publicatzione + Toca inoghe pro agiùnghere fotografias + Formados atzetados: jpeg, png, gif \n \n Mannària màssima de is documentos: 15 MB \n \n Is albums podent cuntènnere finas a 4 fotografias o vìdeos + Càrriga unu mèdiu + Agiunghe una didascalia optzionale + S\'aplicatzione at retzidu unu messàgiu de errore longu meda dae s\'API %1$s + Antiprima de su messàgiu + Agiunghe mentovos in cada messàgiu + Recuperende s\'arresonada + Òrdina pro + Tìtulu pro su vìdeu + Iscrie·ti a Peertube + Tèngio a su mancu 16 annos e atzeto is %1$s de custa istàntzia + Ligàmenes + Càmbia su colore de is ligàmenes (URLs, mentovos, etichetas etz.) in is messàgios + Intestatzione de is cumpartziduras + Càmbia su colore de su nùmene ammustradu in sa parte in artu de is messàgios + Càmbia su colore de su nùmene de s\'utente in sa parte in artu de is messàgios + Càmbia su colore de s\'intestatzione pro is cumpartziduras + Publicatziones + Colore de isfundu de is publicatziones in is lìnias de tempus + Reseta is colores + Toca inoghe pro resetare totu is colores personalizados tuos + Reseta + Iconas + Colores de is iconas de fundu de is lìnias de tempus + Apica custa eticheta + Logotipu de s\'istàntzia + Modìfica su profilu + Faghe un\'atzione + Tradutzione + Antiprima de s\'immàgine + Colore de su testu + Càmbia su colore de su testu in is ricuadros + Àplica is modìficas + Depes torrare a allùghere s\'aplicatzione pro aplicare is modìficas + Torra a allùghere + Imprea unu tema personalizadu + Permite de subraiscrìere is colores de su tema seletzionadu a pitzu + Temas + Sarva in antis + As esportadu su tema + Su tema est istadu esportadu in CSV sena problemas + Àplica su colore primàriu a sa barra de istadu + Colore de sa barra de istadu + Riprìstina unu tema predefinidu + Importa unu tema + Toca inoghe pro importare unu tema dae un\'esportatzione prus betza + Esporta su tema + Toca inoghe pro esportare su tema atuale + Ddoe est istadu un\'errore seletzionende su documentu de su tema + Seletzionadore de temas + Seletziona unu tema pre-installadu + Temas + Àplica su colore primàriu a sa barra de navigatzione + Colore de sa barra de navigatzione + Colore de suta de su cuntenutu de s\'aplicatzione. + Colore de isfundu + Colores de ènfasi pro unas cantas partes de s\'IU. + Colore de ènfasi + Is ammustrados prus a fitianu in s\'aplicatzione tua. + Colore primàriu + Esporta is sinnalibros a s\'istàntzia + Importa is sinnalibros dae s\'istàntzia + Nùmeru de utentes + Nùmeru de istados + Nùmeru de istàntzias + Blocadu + Acaba in %s + Ite dd\'est de nou in sa %s + Podes sighire su contu meu pro agiornamentos + S\'istàntzia no est a disponimentu in https://instances.social + Ammustra su ligàmene intreu + Cumpartzi su ligàmene + As copiadu s\'URL in punta de billete + Aberi cun un\'àtera aplicatzione + Verìfica sa deviatzione + Custu URL non torrat a indiritzare + %1$s \n \n torrat a imbiare cara a\n \n %2$s + Càmbia s\'agente de s\'utente + Imposta un\'agente de s\'utente personalizadu o lassa·lu bòidu + Permitet de personalizare s\'agente de s\'utente impreadu pro is cramadas api o cun su navigadore integradu. + Boga is paràmetros UTM + S\'aplicatzione at a bogare in manera automàtica is paràmetros UTM dae is URL in antis de abèrrere unu ligàmene. + Tendèntzias + Tendèntzias de como + %d persones nde sunt chistionende + Contos de Twitter (pro mèdiu de Nitter) + Nùmenes de utente de Twitter separados dae ispàtzios + Proas de identidade + Identidade verificada + Verificada dae %1$s (%2$s) + Cantzella sa notìfica + Ammustra prus optziones + Est un\'istòria de Pixelfed + Càrriga unu cuntenutu multimediale e s\'at a agiùnghere automaticamente a s\'istòria de Pixelfed tua. + Cuntenutu multimediale agiuntu a s\'istòria tua! + Atzione disabilitada + Acaba de sighire + B\'at àpidu carchi problema. Controlla sa cartella de iscarrigamentu tua in is cunfiguratziones. + Annùntzios + Perunu annùntziu! + Agiunghe una reatzione + Imprea su navigadore preferidu tuo in intro de s\'aplicatzione. Boga sa seletzione a custa funtzionalidade pro abèrrere is ligàmenes esternamente. + Memòria temporànea pro is vìdeos in MB. Zero cheret nàrrere peruna memòria. + Filigranas + Agiunghe automaticamente una filigrana a fundu de is immàgines. Podes personalizare su testu pro cada contu. + Perunu distribudore agatadu! + Tenes bisòngiu de unu distribudore pro retzire notìficas push.\nAs a agatare àteras informatziones in %1$s.\n\nPodes fintzas disabilitare sas notìficas push in sas impostatziones pro ignorare custu messàgiu. + Ischerta unu distribudore + diff --git a/app/src/main/res/values-si/strings.xml b/app/src/main/res/values-si/strings.xml new file mode 100644 index 000000000..99d378f29 --- /dev/null +++ b/app/src/main/res/values-si/strings.xml @@ -0,0 +1,1142 @@ + + + මෙනුව විවෘත කරන්න + මෙනුව වසන්න + පිළිබඳ + About the instance + පෞද්ගලිකත්වය + ගබඩාව + ඉවත් වන්න + පුරන්න + + වසන්න + ඔව් + නැහැ + අවලංගු කරන්න + බාගත කරන්න + %1$s බාගන්න + මාධ්‍යය සුරකින ලදී + ගොනුව: %1$s + මුරපදය + විද්‍යුත් තැපැල් ලිපිනය + ගිණුම් + Toots + හැඳුනුම් සංකේත + සුරකින්න + ප්‍රතිස්ථාපනය කරන්න + ප්‍රතිඵල නොමැත! + Instance + Instance: mastodon.social + දැන් වැඩ කරන්නේ %1$s ගිණුම සමඟින් + ගිණුමක් එක් කරන්න + The content of the toot has been copied to the clipboard + The URL of the toot has been copied to the clipboard + වෙනස් කරන්න + පින්තූරයක් තෝරන්න… + Clean + කැමරාව + සියල්ල මකන්න + Translate this toot. + සැලසුම් කරන්න + අකුරු සහ ලාංඡනවල ප්‍රමාණය + අකුරුවල වර්තමාන ප්‍රමාණය වෙනස් කරන්න: + ලාංඡනවල වර්තමාන ප්‍රමාණය වෙනස් කරන්න: + ඊළඟ + පෙර + සමඟින් විවෘත කරන්න + තහවුරු කරන්න + මාධ්‍යය + සමඟ බෙදාගන්න + Fedilab හරහා බෙදාගන්නා ලදී + ප්‍රතිචාර + පරිශීලක නාමය + කටුසටහන් + ප්‍රියතම + නව අනුගාමිකයන් + සඳහන් කිරීම් + Boosts + Show boosts + ප්‍රතිචාර පෙන්වන්න + Open in browser + පරිවර්තනය කරන්න + කරුණාකර, මෙම ක්‍රියාවට පෙර තත්පර කිහිපයක් රැඳී සිටින්න. + + මුල + ස්ථානීය කාල රේඛාව + ඒකාබද්ධ කාල රේඛාව + විකල්ප + ප්‍රියතම + සන්නිවේදන + නිහඬ කරන ලද පරිශීලකයන් + අවහිර කරන ලද පරිශීලකයන් + දැනුම්දීම් + Follow requests + සැකසුම් + ගිණුමක් මකන්න + %1$s ගිණුම මෘදුකාංගයෙන් මකා දමනවා ද? + විද්‍යුත් ලිපියක් යවන්න + Tap on the path to change it + අසාර්ථකයි! + Scheduled toots + Information below may reflect the user\'s profile incompletely. + Emoji ඇතුල් කරන්න + The app did not collect custom emojis for the moment. + Push notifications + Are you sure you want to logout? + Are you sure you want to logout @%1$s@%2$s? + + No toot to display + No stories to display + Stories + Boosted by %1$s + Add this toot to your favourites? + Remove this toot from your favourites? + Boost this toot? + Unboost this toot? + Pin this toot? + Unpin this toot? + නිහඬ කරන්න + අවහිර කරන්න + වාර්තා කරන්න + ඉවත් කරන්න + පිටපත් කරන්න + බෙදාගන්න + සඳහන් කරන්න + Timed mute + මකාදමා, කටුසටහනකට + + මෙම ගිණුම නිහඬ කරනවා ද? + මෙම ගිණුම අවහිර කරනවා ද? + Report this toot? + මෙම අඩවිය අවහිර කරනවා ද? + Unmute this account? + Unblock this account? + + + දැනුම්දෙන්න + නිශ්ශබ්ද + + + Delete this toot? + Delete & re-draft this toot? + + පොත් යොමු + පොත් යොමුවලට එක් කරන්න + පොත් යොමුව ඉවත් කරන්න + පෙන්වීමට පොත් යොමු නැත + Status has been added to bookmarks! + Status was removed from bookmarks! + + තත්. %d + මිනි.%d + පැය %d + දින %d + + තත්පර %d + තත්පර %d + + + විනාඩි %d + විනාඩි %d + + + පැය %d + පැය %d + + + %d day + %d days + + + අවවාදයයි + ඔබේ සිතුවිලි මොනවාද? + TOOT! + QUEET! + cw + Write a toot + Reply to a toot + Write a queet + Reply to a queet + Select a media + මාධ්‍යය තෝරන අතරතුර දෝෂයක් සිදුවුණා! + මෙම මාධ්‍යය මකනවා ද? + Your toot is empty! + Visibility of the toot + Visibility of the toots by default: + The toot has been sent! + You are replying to this toot: + සංවේදී අන්තර්ගතයක් ද? + + Post to public timelines + Do not post to public timelines + Post to followers only + Post to mentioned users only + + No drafts! + Choose a toot + ගිණුමක් තෝරන්න + ගිණුම් කිහිපයක් තෝරන්න + කටුසටහන ඉවත් කරනවා ද? + Tap on the button to display the original toot + දෘශ්‍යාබාධිත අය සඳහා විස්තර කරන්න + + විස්තරයක් නොමැත! + + %1$s නිකුත්කිරීම + නිර්මාතෘ: + බලපත්‍රය: + GNU GPL V3 + මූලාශ්‍ර කේතය: + Translation of toots: + Search instances: + Icon designer: + + සාකච්ඡාව + + පෙන්වීමට ගිණුමක් නැත + No follow request + Toots \n %1$s + Following \n %1$s + Followers \n %1$s + Pinned \n %d + අවසර දෙන්න + ප්‍රතික්ෂේප කරන්න + + No scheduled toots to display! + Write a toot and then choose Schedule from the top menu. + Delete scheduled toot? + Media: %d + The toot has been scheduled! + The scheduled date must be greater than the current hour! + Battery saver is enabled! It might not work as expected. + + The time for muting should be greater than one minute. + %1$s has been muted until %2$s.\n You can unmute this account from their profile page. + %1$s is muted until %2$s.\n Tap here to unmute the account. + + පෙන්වීමට දැනුම්දීම් නැත + ඔබව සඳහන් කළා + wrote a new message + boosted your status + favourited your status + followed you + asked to follow you + + and another notification + and %d other notifications + + + %d like + %d likes + + දැනුම්දීමක් මකනවා ද? + දැනුම්දීම් සියල්ල මකනවා ද? + දැනුම්දීම මකාදමන ලදී! + දැනුම්දීම් සියල්ල මකාදමන ලදී! + + Following + අනුගාමිකයින් + Pinned + + Unable to get client id! + Unable to connect to instance domain! + අන්තර්ජාල සබඳතාවක් නොමැත! + ගිණුම අවහිර කරන ලදී! + ගිණුම තවදුරටත් අවහිර කර නොමැත! + ගිණුම නිහඬ කරන ලදී! + ගිණුම තවදුරටත් නිහඬ කර නොමැත! + The account was followed! + The account is no longer followed! + The toot was boosted! + The toot is no longer boosted! + The toot was added to your favourites! + The toot was removed from your favourites! + The toot was reported! + The toot was deleted! + The toot was pinned! + The toot was unpinned! + අපොයි! දෝෂයක් සිදුවුණා! + An error occurred! The instance did not return an authorisation code! + The instance domain does not seem to be valid! + ගිණුම් අතර මාරුවනවිට දෝෂයක් සිදුවුණා! + සොයන අතරතුර දෝෂයක් සිදුවුණා! + The profile data have been saved! + කිසිම ක්‍රියාමාර්ගයක් ගැනීමට නොහැක + මාධ්‍යය සුරකින ලදී! + පරිවර්තනය කරන අතරතුර දෝෂයක් සිදුවුණා! + පරිවර්තනයන්, සැකසුම් තුලින් අක්‍රිය කර ඇත + කටුසටහන සුරකින ලදී! + Are you sure this instance allows this number of characters? Usually, this value is close to 500 characters. + Visibility of the toots has been changed for the account %1$s + + Number of toots per load + සැමවිටම + WIFI + අහන්න + Load the media + Load the pictures + තවත් පෙන්වන්න… + අඩුවෙන් පෙන්වන්න… + සංවේදී අන්තර්ගතයකි + Disable GIF avatars + මාර්ගය: + කටුසටහන් ස්වයංක්‍රීයව සුරකින්න + Add URL of media in toots + Notify when someone follows you + Notify when someone boosts your status + Notify when someone favourites your status + Notify when someone mentions you + Notify when a poll ended + Notify for new posts + Show confirmation dialog before boosting + Show confirmation dialog before adding to favourites + Notify in WIFI only + දැනුම් දෙන්න ද? + Silent Notifications + NSFW view timeout (seconds, 0 means off) + Media Description timeout (seconds, 0 means off) + Edit profile + Custom sharing + Your custom sharing URL… + Bio… + Lock account + වෙනස් කිරීම් සුරකින්න + Choose a header picture + Fit preview images + Automatically split toots in replies when chars are over: + ලබා දී ඇති අකුරු 160 ක සීමාවට ඔබ ළඟා වී ඇත! + ලබා දී ඇති අකුරු 30 ක සීමාවට ඔබ ළඟා වී ඇත! + Between + සහ + වේලාව %1$s ට වඩා වැඩි විය යුතු යි + වේලාව %1$s ට වඩා අඩු විය යුතු යි + ආරම්භ වන වේලාව + අවසන් වන වේලාව + Use the built-in browser + Custom tabs + Javascript සක්‍රිය කරන්න + Automatically expand cw + Allow third-party cookies + ඔබේ API යතුර, Yandex සඳහා හිස්ව තැබීමට ඔබට හැකි ය + + අඳුරු + ආලෝකමත් + කළු + + LED වර්ණය තෝරන්න: + + නිල් + මයුර නිල් + දම් පැහැ තද රතු + කොළ + රතු + කහ + සුදු + + Follow + අවහිර නොකරන්න + නිහඬ කරන්න + නිහඬ බව නැති කරන්න + Request sent + Follows you + සොයන්න + First letter in capital for replies + Resize pictures + Resize videos + + Push notifications + Please, confirm push notifications that you want to receive. + You can enable or disable these notifications later in settings (Notifications tab). + + + Clear cache + There are %1$s of data in cache.\n\nWould you like to delete them? + Mb + Cache was cleared! %1$s were released + + Title + Title… + Description + Keywords + Keywords… + + Synchronize + Filter + Your toots + Your notifications + Public + Unlisted + Private + Direct + Some keywords… + Show media + Show pinned + No matching result found! + Backup toots for %1$s + %1$s new toots have been imported + %1$s new notifications have been imported + + Dates descending + Dates ascending + + + නොමැත + Only + Both + + No toots were found in database. Please, use the synchronize button from the menu to retrieve them. + + Recorded data + Only basic information from accounts are stored on the device. + These data are strictly confidential and can only be used by the application. + Deleting the application immediately removes these data.\n + ⚠ Login and passwords are never stored. They are only used during a secure authentication (SSL) with an instance. + + අවසරයන්: + - ACCESS_NETWORK_STATE: Used to detect if the device is connected to a WIFI network.\n + - INTERNET: Used for queries to an instance.\n + - WRITE_EXTERNAL_STORAGE: Used to store media or to move the app on a SD card.\n + - READ_EXTERNAL_STORAGE: Used to add media to toots.\n + - BOOT_COMPLETED: Used to start the notification service.\n + - WAKE_LOCK: Used during the notification service. + + API permissions: + - Read: Read data.\n + - Write: Post statuses and upload media for statuses.\n + - Follow: Follow, unfollow, block, unblock.\n\n + ⚠ These actions are carried out only when user requests them. + + Tracking and Libraries + The application does not use tracking tools (audience measurement, error reporting, etc.) and does not contain any advertising.\n\n + The use of libraries is minimized: \n + - Glide: To manage media\n + - Android-Job: To manage services\n + - PhotoView: To manage images\n + + Translation of toots + The application offers the ability to translate toots using the locale of the device and the Yandex API.\n + Yandex has its proper privacy-policy which can be found here: https://yandex.ru/legal/confidential/?lang=en + + Thank you to: + + Filter out by regular expressions + සෙවීම + ඉවත් කරන්න + Fetch more toots… + + ලැයිස්තු + මෙම ලැයිස්තුව ස්ථිරවම මකා දැමීමට අවශ්‍ය බව විශ්වාස ද? + There is nothing in this list yet. When members of this list post new statuses, they will appear here. + ලැයිස්තුවට එකතු කරන්න + ලැයිස්තුවක් එකතු කරන්න + ලැයිස්තුව මකන්න + ලැයිස්තුව සංස්කරණය කරන්න + New list title + The account was added to the list! + You don\'t have any lists yet! + + %1$s has moved to %2$s + Authentication does not work? + Here are some checks that might help:\n\n + - Check there is no spelling mistakes in the instance name\n\n + - Check that your instance is not down\n\n + - If you use the two-factor authentication (2FA), please use the link at the bottom (once the instance name is filled)\n\n + - You can also use this link without using the 2FA\n\n + - If it still does not work, please raise an issue on GitLab at https://gitlab.com/tom79/mastalab/issues + + Media has been loaded. Tap here to display it. + This action can be quite long. You will be notified when it will be finished. + Still running, please wait… + Export statuses + Export statuses for %1$s + %1$s toots out of %2$s have been exported. + Something went wrong when exporting data for %1$s + Something went wrong when exporting data! + Something went wrong when importing data! + + Proxy + Enable proxy? + Host + Port + Login + Password + Add toot details when sharing + Liberapay මගින් මෙම මෘදුකාංගයට සඳහා ආධාර කරන්න + There is an error in the regular expression! + No timelines was found on this instance! + Delete this instance? + Translate in + Follow instance + You already follow this instance! + The instance is followed! + Partnerships + Information + Hide boosts from %s + Feature on profile + Show boosts from %s + Don\'t feature on profile + The account is now featured on profile + The account is no longer featured on profile + Boosts are now shown! + Boosts are now hidden! + සෘජු පණිවිඩයක් + Filters + No filters to display. You can create one by tapping on the \"+\" button. + Keyword or phrase + Home timeline + පොදු කාල රේඛා + Notifications + Conversations + Will be matched regardless of casing in text or content warning of a toot + Drop instead of hide + Filtered toots will disappear irreversibly, even if filter is later removed + When the keyword or phrase is alphanumeric only, it will only be applied if it matches the whole word + Whole word + Filter contexts + One or multiple contexts where the filter should apply + Expire after + Delete filter? + Update filter + Create filter + Whom to follow + There is no accounts listed for the moment! + Follow + Select all + Unselect all + %s is followed! + Creating the list %s + Adding accounts to the list + Accounts were added to the list + Adding accounts to the list + You have not created a list yet. Tap on the \"+\" button to add a new one. + Who to follow + Trunk API + Account(s) can\'t be followed + Fetching remote account + Automatically expand hidden media + New follow + New Boost + New Favourite + New Mention + Poll Ended + New Toot + Toots Backup + New posts + Media Download + Change notification sound + ගීත කණ්ඩය තෝරාගන්න + Enable time slot + \'කෙසේ ද?\' වීඩියෝ + Fetching remote thread! + අවහිර කරන ලද අඩවි නොමැත! + අඩවිය අවහිර නොකරන්න + %s අවහිර නොකිරීම විශ්වාස ද? + %s අවහිර කිරීම විශ්වාස ද? + අවහිර කරන ලද අඩවි + අඩවිය අවහිර කරන්න + අඩවිය අවහිර කරන ලදී + අඩවිය තවදුරටත් අවහිර කර නොමැත! + Fetching remote status + Comment + Peertube instance + Be the first to leave a comment on this video with the top right button! + නැරඹුම් %s යි + ධාවන කාලය: %s + Add an instance + Comments are not enabled on this video! + විභේදනයක් තෝරන්න + Peertube favourites + The video has been added to bookmarks! + The video has been removed from bookmarks! + There is no Peertube videos in your favourites! + නාලිකාව + වීඩියෝ + Channels + Use Emoji One + Information + Display previews in all toots + New UX/UI designer + Display video previews + The account id has been copied in the clipboard! + භාෂාව වෙනස් කරන්න + ප්රකෘති භාෂාව + Truncate long toots + Truncate toots over \'x\' lines. Zero means disabled. + තවත් පෙන්වන්න + අඩුවෙන් පෙන්වන්න + Manage tags + The tag already exists! + The tag has been stored! + The tag has been changed! + The tag has been removed! + Schedule boost + The boost is scheduled! + No scheduled boost to display! + Schedule boost.]]> + කලා කාල රේඛාව + මෙනුව විවෘත කරන්න + Go back + මෘදුකාංගයේ ලාංඡනය + Profile picture + Profile banner + Contact admin of the instance + Add new + MastoHost ලාංඡනය + Emoji picker + නැවුම් කරන්න + සංවාදය දිගහරින්න + ගිණුමක් ඉවත් කරන්න + අවහිර කළ අඩවිය මකන්න + Custom emoji picker + වීඩියෝව ධාවනය කරන්න + New toot + Image of the card + Hide media + Favicon + Add description for media (for the visually impaired) + + කවදාවත් එපා + මිනිත්තු 30 + පැය 1 + පැය 6 + පැය 12 + දින 1 + සති 1 + + In this field, you need to write your instance host name.\nFor example, if you created your account on https://mastodon.social\nJust write mastodon.social (without https://)\n + You can start writing first letters and names will be suggested.\n\n + ⚠ The Login button will only work if the instance name is valid and the instance is up! + + තවත් තොරතුරු + + භාෂා + මාධ්‍ය පමණයි + Show NSFW + Crowdin translations + Crowdin manager + Translation of the application + About Crowdin + Bot + Pixelfed instance + Mastodon instance + Any of these + All of these + None of these + Any of these words (space-separated) + All these words (space-separated) + Add some words to filter (space-separated) + Change column name + Misskey instance + No app supporting this link is installed on your device. + Subscriptions + Overview + Trending + Recently added + Local + Upload + Reply + Delete a comment + Are you sure to delete this comment? + Full screen video + Mode for videos + Select the file to upload + My videos + Title + License + Category + Language + This video contains mature or explicit content + Enable video comments + Update video + Description + The video has been updated! + Upload cancelled! + The video has been uploaded! + Uploading, please wait… + Tap here to edit the video data. + Delete video + Are you sure to delete this video? + Display NSFW videos + No videos to display! + Leave a comment + Share + Choose a schedule mode + From device + From server + Toots (Server) + Toots (Device) + Modify + Display new toots above the \"Fetch more\" button + කාල රේඛා + Interface + Contacts + %1$s commented your video %2$s]]> + %1$s is following your channel %2$s]]> + %1$s is following your account]]> + %1$s has been published]]> + %1$s succeeded]]> + %1$s failed]]> + %1$s published a new video: %2$s]]> + %1$s has been blacklisted]]> + %1$s has been unblacklisted]]> + දත්ත අපනයනය + දත්ත ආනයනය + Select the file to import + An error occurred when selecting the backup file! + Add a public comment + Send comment + There is no Internet connection. Your message has been stored in drafts. + Plain text + HTML + Markdown + Logout account + All + Support the app + Open Collective enables groups to quickly set up a collective, raise funds and manage them transparently. + Copy link + Connect + Normal + Compact + Console + Set display mode + Patch the Security Provider + Update tracking domains + The tracking data base has been updated! + http calls blocked by the application + List of blocked calls + Submit + The data base has been exported! + Featured hashtags + Filter timeline with tags + No tags + Hide the \"delete\" button in the notification tab + Attach an image when sharing a URL + + Poll + Polls + Create a poll + Choice 1 + Choice 2 + Choice %d + You need two choices at least for the poll! + Done + end at %s + Refresh poll + Vote + A poll you have voted in has ended + A poll you tooted has ended + Customize + Categories + Time slot + Advanced + Display \'new\' badge on unread toots + Peertube + Move timeline + Hide timeline + Reorder timelines + List permanently deleted + Followed instance removed + Pinned tag removed + Undo + You need to keep two visible tabs! + Reorder timelines + Main timelines can only be hidden! + BBCode + Always mark media as sensitive + GNU instance + Cached status + Forward tags in replies + Long press to store media + Blur sensitive media + Display timelines in a list + Display timelines + Mark bot accounts in toots + Manage tags + Remember the position in Home timeline + History + Playlists + Display name + You don\'t have any playlists. Tap on the \"+\" icon to add a new playlist + You must provide a display name! + The channel is required when the playlist is public. + Create a playlist + There is nothing in this playlist yet. + redo + Gallery + Emoji + Sticker + Eraser + Text + Filter + Brush + Are you sure you want to exit without saving the image? + Discard + Saving… + Image Saved Successfully! + Failed to save Image + Opacity + Enable photo editor + Add a poll item + Remove last poll item + Mute conversation + Unmute conversation + The conversation is no longer muted! + The conversation is muted + Open application features + Timed mute + Mention the account + Refresh cache + Mention the status + News + General + Regional + Art + Journalism + Activism + Gaming + Technology + Adult content + Furry + Food + Logo of the instance + Something went wrong when checking available instances! + Join Mastodon + Choose an instance by picking up a category, then tap on a check button. + Choose an instance by tapping on a check button. + %1$s users + Confirm password + I agree to %1$s and %2$s + server rules + terms of service + Sign up + This instance works with invitations. Your account will need to be manually approved by an administrator before being usable. + Please, fill all the fields! + Passwords don\'t match! + The email doesn\'t seem to be valid! + Your username will be unique on %1$s + You will be sent a confirmation e-mail + Use at least 8 characters + Password should contain at least 8 characters + Username should only contain letters, numbers and underscores + Account created! + Your account has been created!\n\n + Think to validate your email within the 48 next hours.\n\n + You can now connect your account by writing %1$s in the first field and click on Connect.\n\n + Important: If your instance required validation, you will receive an email once it is validated! + + Save the message in drafts? + Administration + Reports + No reports to display! + Reconnect the account + The application failed to access the admin features. You might need to reconnect the account for having the correct scope. + Unresolved + Remote + Active + Pending + Disabled + Silenced + Suspended + Permissions + Email status + Login status + Joined + Most recent IP + Warn + Disable + Silence + Notify the user per e-mail + Custom warning + User + Moderator + Administrator + Confirmed + Not confirmed + Reported statuses + Account + Undo silence + Undo disable + Suspend + Undo suspend + The account is silenced! + The account is no longer silenced! + The account is suspended! + The account is no longer suspended! + The account is disabled! + The account is no longer disabled! + The account has been warned! + Display the admin menu + Display the admin feature in statuses + Allow + The account is approved! + The account is rejected! + Assign to me + Unassign + Mark as resolved + Mark as unresolved + Empty content! + Display Fedilab features button + The application needs to access audio recording + Voice message + Enable quick reply + The account you are replying might not see your message! + If disabled, the app will always load last statuses + If disabled, sensitive media will be hidden with a button + Store media in full size with a long press on previews + Add an ellipse button at the top right for listing all tags/instances/lists + During the time slot, the app will send notifications. You can reverse (ie: silent) this time slot with the right spinner. + Display a Fedilab button below profile picture. It is a shortcut for accessing in-app features. + Allow to reply directly in timelines below statuses + Previews will not be cropped in timelines + Allow to play embedded videos directly in timelines + Allow to reverse the way to read statuses that are displayed once tapping the fetch more button + This option allows to support recent cipher suites. It is useful for older Android devices or if you cannot connect to your instance. + Exclusively for Peertube videos. Switch this mode if you cannot play them. + These tags will allow to filter statuses from profiles. You will have to use the context menu for seeing them. + Automatically insert a line break after the mention to capitalize the first letter + Allow content creators to share statuses to their RSS feeds + Compose + Maximum retry times when uploading media + Create a new Folder here + Enter the folder name + Please enter a valid folder name + This folder already exists.\n Please provide another name for the folder + Select + Default Directory + Folder + Create folder + Display a toast message after an action has been completed (boost, fav, etc.)? + Muted instances have been exported! + Add an instance + Export instances + Import instances + Crash reports + Enable crash reports + If enabled, a crash report will be created locally and then you will be able to share it. + Fedilab has stopped :( + You can send me by email the crash report. It will help to fix it :)\n\nYou can add additional content. Thank you! + Use the wysiwyg + When enabled, you will be able to format your text easily with tools. + Statistics + Total statuses + Number of boosts + Number of favourites + Number of mentions + Number of follows + Number of polls + Number of replies + Number of statuses + Statuses + Visibility + Number with media + Number with sensitive media + Number with CW + First status date + Last status date + First notification date + Last notification date + Frequency + %s statuses per day + %s notifications per day + Date range + Groups + No groups! + Disable custom animated emojis + Charts + Display charts + The application collects your local data, please wait… + Backup + Auto backup statuses + This option is per account. It will launch a service that will automatically store your statuses locally in the database. That allows to get statistics and charts + Auto backup notifications + This option is per account. It will launch a service that will automatically store your notifications locally in the database. That allows to get statistics and charts + Report account + Send an invitation + Your instance does not allow to register a new account! + + %d vote + %d votes + + + %d voter + %d voters + + + Single choice + Multiple choices + + + 5 minutes + 30 minutes + 1 hour + 6 hours + 1 day + 3 days + 7 days + + + Webview + Direct stream + + For joining my instance \"%1$s\", you can download Fedilab:\n\nF-Droid: %2$s\nGoogle: %3$s\n\nThen open the link below with Fedilab and create your account :)\n\n%4$s + + Your poll can\'t have duplicated options! + For all accounts + Database cache + Clear your home timeline cache + Clear your cached statuses + Clear your bookmarks + Files in cache + Total notifications + Hide menu items + Fedilab is running live notifications + For %1$s accounts with %2$s events + Live notifications for %1$s + Live notifications will be enabled for this account. + Clear cache when leaving + The cache (media, cached messages, data from the built-in browser) will be automatically cleared when leaving the application. + Do you want to unfollow this account? + Show confirmation dialog before unfollowing + Replace Youtube with Invidio.us + Invidious is an alternative front-end to YouTube + Enter your custom host or leave blank for using invidious.snopyta.org + Replace Twitter with Nitter + Nitter is an open source alternative Twitter front-end focused on privacy. + Enter your custom host or leave blank for using nitter.net + Replace Instagram with Bibliogram + Bibliogram is an open source alternative Instagram front-end focused on privacy. + Enter your custom host or leave blank for using bibliogram.art + Replace Reddit with Libreddit + Libreddit is an open source alternative Reddit front-end focused on privacy. + Enter your custom host or leave blank for using libredd.it + Replace Medium links + Replace medium.com links with an open source alternative front-end focused on privacy. + Default: scribe.rip + Replace Wikipedia links + Replace Wikipedia link with an open source alternative front-end focused on privacy. + Default: wikiless.org + Hide Fedilab notification bar + For hiding the remaining notification in the status bar, tap on the eye icon button then uncheck: \"Display in status bar\" + Use a push notifications system for getting notifications in real time. + No live notifications + Live notifications + Notifications will be fetched every 15 minutes. + Add notes + Notes for the account + Allow to compress large photos into smaller sized photos with very less or negligible loss in quality of the image. + Allow compressing videos while maintaining their quality. + The app is compressing the media, it can be quite long… + Change app icon + Tap to change the app icon + Post + Visibility of the post + Tap here to add photos + Accepted Formats: jpeg, png, gif \n\nMax File Size: 15 MB \n\nAlbums can contain up to 4 photos or videos + Upload media + Add an optional caption + The app received a very long error message from the API %1$s + Message preview + Add mentions in each message + Fetching conversation + Order by + Title for the video + Join Peertube + I am at least 16 years old and agree to the %1$s of this instance + Links + Change the color of links (URLs, mentions, tags, etc.) in messages + Reblogs header + Change the color of display name at the top of messages + Change the color of the user name at the top of messages + Change the color of the header for reblogs + Posts + Background color of posts in timelines + Reset colors + Tap here to reset all your custom colors + Reset + Icons + Color of bottom icons in timelines + Pin this tag + Logo of the instance + Edit profile + Make an action + Translation + Image preview + Text color + Change the text color in pots + Apply changes + You need to restart the application to apply changes + Restart + Use a custom theme + Allow to override colors of the selected theme above + Theming + Store before + The theme was exported + The theme has been successfully exported in CSV + Apply the primary color to the status bar + Status bar color + Restore a default theme + Import a theme + Tap here to import a theme from a previous export + Export the theme + Tap here to export the current theme + An error occurred when selecting the theme file + Theme Picker + Select a pre-installed theme + Themes + Apply the primary color to the navigation bar + Navigation bar color + The underlying color of the app’s content. + Background color + Accents select parts of the UI. + Accent color + Displayed most frequently across your app. + Primary color + Export bookmarks to the instance + Import bookmarks from the instance + User count + Status count + Instance count + Blocked + End in %s + What\'s new in %s + You can follow my account for updates + This instance is not available on https://instances.social + Display full link + Share link + The URL has been copied to the clipboard + Open with another app + Check redirect + This URL does not redirect + %1$s \n\nredirects to\n\n %2$s + Change the user agent + Set a custom user agent or leave blank + Allows to customize the user agent used for api calls or with the built-in browser. + Remove UTM parameters + The app will automatically remove UTM parameters from URLs before visiting a link. + Trends + Trending now + %d people talking + Twitter accounts (via Nitter) + Twitter usernames space separated + Identity proofs + Verified identity + Verified by %1$s (%2$s) + Delete the notification + Display more options + It is a Pixelfed story + Upload a media, it will be automatically added to your Pixelfed story. + Media successfully added to your story! + Action disabled + Unfollow + Something went wrong, please check your download directory in settings. + Announcements + No announcements! + Add a reaction + Use your favourite browser inside the app. Uncheck this feature to open links externally. + Video cache in MB, zero means no cache. + Watermarks + Automatically add a watermark at the bottom of pictures. The text can be customized for each account. + No distributors found! + You need a distributor for receiving push notifications.\nYou will find more details at %1$s.\n\nYou can also disable push notifications in settings for ignoring that message. + Select a distributor + diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml new file mode 100644 index 000000000..2f3578f4d --- /dev/null +++ b/app/src/main/res/values-sl/strings.xml @@ -0,0 +1,1158 @@ + + + Odpri meni + Zapri meni + Vizitka + Podatki o tej instanci + Zasebnost + Predpomnilnik + Odjavi me + Prijava + + Zapri + Da + Ne + Prekliči + Prenesi + Prenos %1$s + Vsebina je shranjena + Datoteka: %1$s + Geslo + Naslov e-pošte + Računi + Objave Toots + Oznake + Shrani + Obnovi + Ni rezultatov. + Instanca strežnika + Instanca: mastodon.social + Zdaj deluje na računu %1$s + Dodajte račun + Vsebina objave je bila kopirana v odložišče + The URL of the toot has been copied to the clipboard + Spremeni + Izberite sliko … + Počisti + Kamera + Izbriši vse + Prevedi to objavo. + Nastavi čas objave + Velikost besedila in ikon + Spremeni trenutno velikost besedila: + Spremeni trenutno velikost ikon: + Naprej + Nazaj + Odpri s pomočjo + Potrdi + Vsebine + Deli z + Deljeno z Fedilab + Odgovori + Uporabniško ime + Osnutki + Priljubljeni + Novi spremljevalci + Omembe + Ojačitve + Prikaži ojačanja + Prikaži odgovore + Odpri v brskalniku + Prevedi + Prosimo, počakajte nekaj sekund preden opravite to dejanje. + + Domov + Lokalna časovnica + Združena časovnica + Možnosti + Priljubljeni + Sporočanje + Utišani uporabniki + Blokirani uporabniki + Obvestila + Zahteve za spremljanje + Nastavitve + Brisanje računa + Ali želite iz aplikacije izbrisati račun %1$s? + Pošlji e-pošto + Pritisnite na pot, če jo želite spremeniti. + Ni uspelo. + Načrtovane objave + Našteti podatki lahko nepopolno odražajo uporabnikov profil. + Vstavi čustvenčka + Aplikacija trenutno še nima zbranih čustvenčkov po meri. + Push notifications + Are you sure you want to logout? + Are you sure you want to logout @%1$s@%2$s? + + Ni objav za prikaz + No stories to display + Stories + Ojačano za %1$s + Naj dodam to objavo med priljubljene? + Naj odstranim to objavo iz priljubljenih? + Naj ojačam to objavo? + Naj utišam to objavo? + Naj pripnem to objavo? + Naj odpnem to objavo? + Utišaj + Blokiraj + Prijavi objavo + Odstrani + Kopiraj + Deli + Citiraj + Začasno utišaj + Zibriši in začni znova + + Naj utišam ta račun? + Naj blokiram ta račun? + Želite prijaviti sporne vsebine v tej objavi? + Želite blokirati to domeno? + Unmute this account? + Unblock this account? + + + Obvesti + Tiho + + + Želite odstraniti to objavo? + Želite izbrisati to objavo in začeti znova? + + Zaznamki + Dodaj med zaznamke + Odstrani zaznamek + Nimate nobenega zaznamka za prikaz + Status je dodan med zaznamke. + Status je odstranjen iz zaznamkov. + + %d s + %d m + %d ur + %d dni + + %d second + %d seconds + %d seconds + %d seconds + + + %d minute + %d minutes + %d minutes + %d minutes + + + %d hour + %d hours + %d hours + %d hours + + + %d day + %d days + %d days + %d days + + + Opozorilo + O čem razmišljate? + TOOT! + QUEET! + cw + Objavite nov Toot + Odgovorite na Toot + Write a queet + Reply to a queet + Izberite vsebino + Prišlo je do napake ob izbiranju vsebine. + Želite izbrisati to vsebino? + Vaš Toot je prazen. + Vidnost objave Toot + Privzeta vidnost objav Toot: + Objava Toot je poslana. + Odgovarjate na to objavo: + Občutljiva vsebina? + + Objavi na javne časovnice + Ne objavi na javne časovnice + Objavi le spremljevalcem + Objavi le omenjenim uporabnikom + + Ni osnutkov. + Izberi objavo Toot + Izberite račun + Izberite nekaj računov + Naj odstranim osnutek? + Pritisnite na gumb, da prikažete izvorno objavo Toot + Opis za slepe in slabovidne + + Opis ni na voljo. + + Različica %1$s + Razvijalec: + Licenca: + GNU GPL V3 + Izvorna koda: + Prevajanje objav: + Iskanje instanc: + Oblikovalec ikone: + + Pogovor + + Za prikaz ni nobenega računa + Ni zahtev za spremljanje + Objave Toot \n %1$s + Spremljate \n %1$s + Spremljevalci \n %1$s + Pripeto \n %d + Avtorizacija + Zavrni + + Nimate časovno načrtovanih objav Toot. + Sestavite objavo Toot in nato izberite Nastavi čas objave iz menija. + Želite izbrisati načrtovano objavo? + Vsebine: %d + Čas objave Toot-a je na urniku. + Planiran čas objave morate določiti kasneje od tega trenutka. + Omogočen je ohranjevalnik baterije. Delovanje lahko odstopa od pričakovanega. + + Čas za utišanje računa naj bo daljši od minute. + %1$s je utišan do %2$s.\nGovor lahko dovolite na profilu uporabnika. + %1$s je utišan do %2$s.\nPritisnite tu, če želite omogočiti govor. + + Ni obvestil za prikaz + vas omenja + wrote a new message + je ojačal tvoj status + je všeč tvoja objava + spremlja vas + asked to follow you + + in še eno obvestilo + in še %d obvestili + in še %d obvestil + in še %d obvestil + + + %d like + %d likes + %d likes + %d likes + + Naj izbrišem to obvestilo? + Naj izbrišem vsa obvestila? + Obvestilo je izbrisano. + Vsa obvestila so izbrisana. + + Spremljanje + Spremljevalci + Pripeto + + Ni mogoče pridobiti ID odjemalca. + Unable to connect to instance domain! + Ni internetne povezave. + Račun je blokiran. + Račun ni več blokiran. + Račun je utišan. + Računu je omogočen govor. + Ta račun ima spremljevalce. + Ta račun nima več spremljevalcev. + Objava je bila ojačana. + Objava ni več ojačana. + Objava Toot je dodana med priljubljene. + Objava odstranjena iz priljubljenih. + Prijavljeno zaradi sporne vsebine. + Objava Toot je izbrisana. + Objava je pripeta. + Objava ni več pripeta. + Ups, prišlo je do napake. + Prišlo je do napake. Instanca ni vrnila avtorizacijske kode. + Domena strežniške instance ni videti veljavna. + Med preklopom med računi je prišlo do napake. + Med iskanjem je prišlo do napake. + Podatki o profilu so shranjeni. + Nobena akcija ni na voljo + Vsebina je shranjena. + Med prevajanjem je prišlo do napake. + Translations are disabled in settings + Osnutek je shranjen. + Ali ste prepričani, da ta instanca omogoča to število znakov? Običajno je ta vrednost približno 500 znakov. + Spremenjena je vidnost objav Toot na računu %1$s + + Količina objav Toot na vsak prenos + Vedno + WiFi + Vprašaj me + Nalaganje vsebin + Nalaganje slik + Prikaži več … + Prikaži manj … + Občutljiva vsebina + Onemogoči animirane avatarje GIF + Pot: + Samodejno shranjevanje osnutkov + Dodaj naslove URL za vsebine v objavah Toot + Obveščaj me o novih spremljevalcih + Obveščaj me, ko nekdo ojača moj status + Obveščaj me, ko kdo moj status doda med priljubljene + Obveščaj me, ko me kdo omeni + Notify when a poll ended + Notify for new posts + Vprašaj me za potrditev preden ojačam + Pred dodajanjem med priljubljene me vprašaj za potrditev + Obveščanj le v omrežju WiFi + Obveščanje? + Tiha obvestila + Časovna omejitev prikaza NSFW (v sekundah, 0 za izklop) + Media Description timeout (seconds, 0 means off) + Urejanje profila + Custom sharing + Your custom sharing URL… + Življenjepis … + Zakleni račun + Shrani spremembe + Izberi sliko za glavo strani + Prilagajaj velikost slike + Objave, ki presegajo 500 znakov, samodejno razbij v odgovre + Dosegli ste 160 znakov, ki so vam na voljo. + Dosegli ste 30 znakov, ki so vam na voljo. + Med + in + Čas mora biti večji od %1$s + Čas mora biti manjši od %1$s + Začetni čas + Končni čas + Uporabljaj vgrajeni brskalnik + Zavihki po meri + Omogoči JavaScript + Samodejno razširi cw + Dovoli tuje piškotke + Tvoj ključ API. Za Yandex je lahko prazen. + + Temna + Svetla + Črna + + Barva lučke LED: + + Modra + Zelenomodra + Vijolična + Zelena + Rdeča + Rumena + Bela + + Spremljaj + Odblokiraj + Utišaj + Dovoli govor + Zahteva je poslana + Vas spremlja + Išči + Odgovore začni z veliko začetnico + Prilagajanje velikosti slik + Resize videos + + Potisna obvestila + Prosimo, potrdite potisna obvestila, ki jih želite prejemati. + Te nastavitve lahko kasneje omogočite ali onemogočite v nastavitvah (na zavihku Obvestila). + + + Počisti predpomnilnik + V predpomnilniku je %1$s podatkov.\n\nAli jih želite izbrisati? + Mb + Predpomnilnik je izpraznjen. Sproščenih je: %1$s + + Naslov + Naslov… + Opis + Ključne besede + Ključne besede… + + Sinhroniziraj + Filtriraj + Tvoje objave + Your notifications + Javno + Ni na seznamu + Zasebno + Neposredno + Nekaj ključnih besed … + Prikaz vsebin + Prikaži pripeto + Ne najdem ujemanj. + Arhiviraj objave za %1$s + Uvoženih je %1$s novih objav Toot + %1$s new notifications have been imported + + Dates descending + Dates ascending + + + Ne + Samo + Oboje + + V podatkovni zbirki ni objav Toot. Prosimo, uporabite gumb Sinhroniziraj v meniju, da jih pridobite. + + Zabeleženi podatki + Le osnovni podatki o računih se hranijo na napravi. + Ti podatki so strogo zaupne narave in jih lahko uporablja le aplikacija. + Ob brisanju aplikacije takoj odstranite tudi vse podatke.\n + ⚠ Prijavni podatki in gesla se nikoli ne shranjujejo. Uporabljeni so le med varno avtentifikacijo (SSL) z instanco. + + Dovoljenja: + - ACCESS_NETWORK_STATE: Se uporablja za zaznavo ali je naprava povezana v omrežje WiFi.\n + - INTERNET: Se uporablja za poizvedbe na strežniški instanci.\n + - WRITE_EXTERNAL_STORAGE: Se uporablja za shranjevanje vsebin ali za možnost premika aplikacije na SD-kartico.\n + - READ_EXTERNAL_STORAGE: Se uporablja za dodajanje vsebin v objave Toot.\n + - BOOT_COMPLETED: Se uporablja za zagon storitve za obveščanje.\n + - WAKE_LOCK: Se uporalbja med delovanjem storitve za obveščanje. + + Dovoljenja za API: + - Branje: Branje podatkov.\n + - Pisanje: Objava statusov in prilaganje vsebin le-tem.\n + - Spremljanje: Spremljanje, prekinitev spremljanj, blokiranje, odblokiranje.\n\n + ⚠ Te akcije se izvedejo le, ko jih zahteva uporabnik. + + Sledenje in knjižnice + Aplikacija ne uporablja analitičnih orodij (merjenje občinstva, spremljanje napak, itd.) in ne vsebuje oglasov.\n\n + Uporaba knjižnic je omejena na: \n + - Glide: za uporavljanje medijskih vsebin\n + - Android-Job: za upravljanje sistemskih storitev\n + - PhotoView: za upravljanje slik\n + + Prevajanje objav Toot + Aplikacija omogoča prevajanje objav Toot s pomočjo nastavitev jezika vaše naprave in API-vmesnika Yandex.\n + Yandex ima svoj Pravilnik o zasebnosti, ki ga lahko najdete na naslovu: https://yandex.ru/legal/confidential/?lang=en + + Zahvala: + + Filtriraj s pomočjo regularnih izrazov + Išči + Izbriši + Zajemi več objav Toot … + + Seznami + Ste prepričani, da želite dokončno izbrisati ta seznam? + Na tem seznamu še ni ničesar. Na njem bodo vidni statusi članov, ko le-ti objavijo nove statuse. + Dodaj na seznam + Dodaj seznam + Izbriši seznam + Uredi seznam + Naslov novega seznama + The account was added to the list! + You don\'t have any lists yet! + + %1$s je premaknjen na %2$s + Prijava ne deluje? + Sledi nekaj točk, ki so vam lahko v pomoč:\n\n + - Preverite, da nimate napak v imenu instance.\n\n + - Preverite, da je instanca deujoča.\n\n + - Če uporabljate dvostopenjsko prijavljanje (2FA), uporabite spodnjo povezavo (zatem, ko je vpisano ime instance).\n\n + - To povezavo lahko uporabite tudi brez dvostopenjskega prijavljanja.\n\n + - Če našteto ne pomaga, prosimo, prijavite napako na Framagit naslovu: https://framagit.org/tom79/fedilab/issues + + Vsebina je naložena. Pritisnite tu, da jo prikažete. + Ta akcija lahko traja precej dolgo. Obvestili vas bomo, ko bo zaključeno. + Se še izvaja. Prosimo, počakajte … + Izvoz statusov + Izvoz statusov za %1$s + Izvoženih je %1$s objav Toot od skupno %2$s. + Med izvažanjem podatkov za %1$s je nekaj šlo narobe + Something went wrong when exporting data! + Something went wrong when importing data! + + Posredniški strežnik + Omogočim proksi? + Gostitelj + Vrata + Prijava + Geslo + Ob deljenju dodaj podrobnosti objave Toot + Podprite to aplikacijo na Liberapay + Napaka v regularnem izrazu. + Na tej instanci ne najdem časovnic. + Izbrišem to instanco? + Prevedi v + Spremljanje instance + To instanco že spremljate. + Ta instanca je spremljana. + Partnerstvo + Informacije + Skrij ojačanja pred %s + Izpostavi v profilu + Prikaži ojačanja uporabnika %s + Ne izpostavljaj v profilu + Račun je izpostavljen v profilu + Račun ni več izpostavljen v profilu + Ojačanja so zdaj prikazana. + Ojačanja so zdaj skrita. + Neposredno sporočilo + Filtri + Nimate filtrov. Pritisnite gumb \"+\", da ustvarite filter. + Ključna beseda ali fraza + Domača časovnica + Javne časovnice + Obvestila + Pogovori + Ujemanje velja neglede na male/velike črke in neglede na opozorila o vsebini objav Toot. + Namesto skrivanja zavrži + Filtrirane objave Toot bodo nepovratno izginile, tudi če filter kasneje odstranite + Ko je ključna beseda ali fraza samo alfanumerična, bo filter uporabljen le, če se ujema cela beseda. + Cela beseda + Konteksti filtra + En ali več kontekstov na katerih naj se izvaja ta filter + Poteče čez + Brisanje filtra? + Posodobitev filtra + Ustvarjanje filtra + Koga naj spremlajm + Trenutno na seznamu ni računov. + Spremljaj + Izberi vse + Odstrani izbor + %s je spremljan. + Ustvarjanje seznama %s + Dodajanje računov na seznam + Računi so dodani na seznam + Dodajanje računov na seznam + Seznama še niste ustvarili. Pritisnite gumb \"+\", da ustvarite nov seznam. + Koga naj spremlajm + Trunk API + Tega računa ne morete spremljati + Pridobivanje oddaljenega računa + Samodejno razširi skirte vsebine + Novi spremljevalci + Novo ojačanje + Nov všeček + Nova omemba + Poll Ended + Nova objava Toot + Arhivirani Tooti + New posts + Prenos vsebine + Spremeni zvok ob obvestilih + Izberite zvok + Omogoči časovni okvir + Videonavodila + Pridobivanje oddaljenih pogovorov. + Ni blokiranih domen. + Odstranitev blokade domene + Res želite odstraniti blokado %s? + Res želite blokirati %s? + Blokirane domene + Blokada domene + Domena je blokirana + Domena ni več blokirana. + Pridobivanje oddaljenih statusov + Komentiraj + Instanca Peertube + S pritiskom na gumb zgoraj desno lahko kot prvi objavite komentar za ta video. + %s ogledov + Trajanje: %s + Dodaj instanco + Na tem videu komentarji niso omogočeni. + Izberite ločljivost + Priljubljeni na Peertube + Video je dodan med zaznamke. + Video je odstranjen iz zaznamkov. + V vaših priljubljenih nimate videov Peertube. + Kanal + Videi + Kanali + Uporabi čustvenčke Emoji One + Informacije + Prikaži predoglede v vseh objavah + Nov oblikovalec uporabniške izkušnje + Prikaži predogled videov + ID računa je kopiran na odložišče. + Spremeni jezik + Privzeti jezik + Posekaj dolge objave + Posekaj objave, ki presegajo \"x\" vrstic. Nič pomeni onemogočeno. + Prikaži več + Prikaži manj + Upravljanje oznak + Oznaka že obstaja. + Oznaka je shranjena. + Oznaka je spremenjena. + Oznaka je izbrisana. + Doliči čas ojačanja + Določili ste čas ojačanja. + Nimate časovno načrtovanih ojačanj. + Doliči čas ojačanja.]]> + Umetniška časovnica + Odpri meni + Go back + Logotip aplikacije + Profilna slika + Plakat profila + Kontaktirajte skrbnika te instance + Dodaj novo + Logotip MastoHost + Nabor čustvenčkov + Osveži + Razširi ta pogovor + Odstrani račun + Izbriši blokirano domeno + Nabor čustvenčkov po meri + Predvajaj video + Nova objava + Slika kartice + Skrij vsebino + Mala ikona + Vsebina za dodajanje opisa + + Nikoli + 30 minut + 1 uro + 6 ur + 12 ur + 1 dan + 1 teden + + V to polje vpišite ime gostitelja vaše instance.\nNaprimer: Če ste ustarili račun na strežniku: https://mastodon.social\nVpišite le ime:mastodon.social (brez https://)\n + Ob vpisu prvih nekaj črk vam bomo podali namige možnih imen gostiteljev.\n\n + ⚠ Gumb Prijava deluje le, ko je instanca strežnika veljavna in delujoča! + + Več informacij + + Jeziki + Samo vsebine + Prikaži NSFW + Prevodi na Crowdin-u + Skrbnik na Crowdin-u + Prevod aplikacije + Več o platformi Crowdin + Robot + Instanca Pixelfed + Instanca Mastodon + Karkilo od naštetega + Vse našteto + Nič od naštetega + Katerakoli od teh besed (ločene s presledkom) + Vse te besede (ločene s presledkom) + Add some words to filter (space-separated) + Preimenuj stolpec + Instanca Misskey + Na vaši napravi ni nameščene aplikcaije, ki podpira to povezavo. + Subscriptions + Overview + Trending + Recently added + Lokalno + Naloži + Odgovori + Izbriši komentar + Ste prepričani, da želite izbrisati ta komentar? + Celozaslonski video + Način za videoposnetke + Izberite datoteko za nalaganje + Moji videi + Naslov + Licenca + Kategorija + Jezik + Ta video vsebuje eksplicitne vsebine za odrasle + Omogoči komentiranje videa + Posodobi video + Opis + Video je posodobljen. + Nalaganje je preklicano. + Vdeoposnetek je naložen. + Nalagam. Prosim, počakajte … + Kliknite tukaj, če želite urediti podatke videa. + Delete video + Are you sure to delete this video? + Display NSFW videos + No videos to display! + Dodaj komentar + Share + Choose a schedule mode + From device + From server + Toots (Server) + Toots (Device) + Spremeni + Display new toots above the \"Fetch more\" button + Časovnice + Vmesnik + Stiki + %1$s commented your video %2$s]]> + %1$s is following your channel %2$s]]> + %1$s is following your account]]> + %1$s has been published]]> + %1$s succeeded]]> + %1$s failed]]> + %1$s published a new video: %2$s]]> + %1$s has been blacklisted]]> + %1$s has been unblacklisted]]> + Export data + Import Data + Select the file to import + An error occurred when selecting the backup file! + Add a public comment + Send comment + There is no Internet connection. Your message has been stored in drafts. + Plain text + HTML + Markdown + Logout account + Vse + Support the app + Open Collective enables groups to quickly set up a collective, raise funds and manage them transparently. + Kopiraj povezavo + Poveži + Običajno + Zgoščeno + Konzola + Nastavi način prikaza + Patch the Security Provider + Update tracking domains + The tracking data base has been updated! + http calls blocked by the application + List of blocked calls + Potrdi + The data base has been exported! + Featured hashtags + Filter timeline with tags + Ni oznak + Hide the \"delete\" button in the notification tab + Attach an image when sharing a URL + + Anketa + Ankete + Ustvari anketo + Izbira 1 + Izbira 2 + Izbira %d + You need two choices at least for the poll! + Opravljeno + end at %s + Refresh poll + Glasuj + A poll you have voted in has ended + A poll you tooted has ended + Prilagodi + Kategorije + Časovni okvir + Napredno + Display \'new\' badge on unread toots + Peertube + Move timeline + Hide timeline + Reorder timelines + List permanently deleted + Followed instance removed + Pinned tag removed + Undo + You need to keep two visible tabs! + Reorder timelines + Main timelines can only be hidden! + BBCode + Always mark media as sensitive + GNU instance + Cached status + Forward tags in replies + Long press to store media + Zameglitev občutljivih podatkov + Display timelines in a list + Display timelines + Mark bot accounts in toots + Manage tags + Remember the position in Home timeline + Zgodovina + Seznami predvajanja + Prikazno ime + You don\'t have any playlists. Tap on the \"+\" icon to add a new playlist + You must provide a display name! + The channel is required when the playlist is public. + Create a playlist + There is nothing in this playlist yet. + redo + Galerija + Smeški + Nalepke + Radirka + Besedilo + Filter + Čopič + Are you sure you want to exit without saving the image? + Opusti + Shranjevanje … + Slika je uspešno shranjena. + Failed to save Image + Prosojnost + Enable photo editor + Add a poll item + Remove last poll item + Mute conversation + Unmute conversation + The conversation is no longer muted! + The conversation is muted + Open application features + Timed mute + Mention the account + Refresh cache + Mention the status + Novice + Splošno + Regionalno + Art + Journalism + Activism + Gaming + Technology + Adult content + Furry + Food + Logo of the instance + Something went wrong when checking available instances! + Join Mastodon + Choose an instance by picking up a category, then tap on a check button. + Choose an instance by tapping on a check button. + %1$s users + Confirm password + I agree to %1$s and %2$s + server rules + terms of service + Sign up + This instance works with invitations. Your account will need to be manually approved by an administrator before being usable. + Please, fill all the fields! + Passwords don\'t match! + The email doesn\'t seem to be valid! + Your username will be unique on %1$s + You will be sent a confirmation e-mail + Use at least 8 characters + Password should contain at least 8 characters + Username should only contain letters, numbers and underscores + Account created! + Your account has been created!\n\n + Think to validate your email within the 48 next hours.\n\n + You can now connect your account by writing %1$s in the first field and click on Connect.\n\n + Important: If your instance required validation, you will receive an email once it is validated! + + Save the message in drafts? + Administration + Poročila + No reports to display! + Reconnect the account + The application failed to access the admin features. You might need to reconnect the account for having the correct scope. + Nerazrešeno + Oddaljeno + Aktivno + V teku + Onemogočeno + Utišano + Suspended + Dovoljenja + Stanje e-pošte + Stanje računa + Pridružen + Zadnji znan IP-naslov + Opozori + Onemogoči + Utišaj + Obvesti uporabnika po e-pošti + Custom warning + Uporabnik + Moderator + Skrbnik + Potrjeno + Nepotrjeno + Reported statuses + Račun + Undo silence + Undo disable + Suspend + Undo suspend + The account is silenced! + The account is no longer silenced! + The account is suspended! + The account is no longer suspended! + The account is disabled! + The account is no longer disabled! + The account has been warned! + Display the admin menu + Display the admin feature in statuses + Allow + The account is approved! + The account is rejected! + Assign to me + Unassign + Mark as resolved + Mark as unresolved + Empty content! + Display Fedilab features button + The application needs to access audio recording + Voice message + Enable quick reply + The account you are replying might not see your message! + If disabled, the app will always load last statuses + If disabled, sensitive media will be hidden with a button + Store media in full size with a long press on previews + Add an ellipse button at the top right for listing all tags/instances/lists + During the time slot, the app will send notifications. You can reverse (ie: silent) this time slot with the right spinner. + Display a Fedilab button below profile picture. It is a shortcut for accessing in-app features. + Allow to reply directly in timelines below statuses + Previews will not be cropped in timelines + Allow to play embedded videos directly in timelines + Allow to reverse the way to read statuses that are displayed once tapping the fetch more button + This option allows to support recent cipher suites. It is useful for older Android devices or if you cannot connect to your instance. + Exclusively for Peertube videos. Switch this mode if you cannot play them. + These tags will allow to filter statuses from profiles. You will have to use the context menu for seeing them. + Automatically insert a line break after the mention to capitalize the first letter + Allow content creators to share statuses to their RSS feeds + Compose + Maximum retry times when uploading media + Create a new Folder here + Enter the folder name + Please enter a valid folder name + This folder already exists.\n Please provide another name for the folder + Select + Default Directory + Folder + Create folder + Display a toast message after an action has been completed (boost, fav, etc.)? + Muted instances have been exported! + Add an instance + Export instances + Import instances + Crash reports + Enable crash reports + If enabled, a crash report will be created locally and then you will be able to share it. + Fedilab se je ustavil :( + Poročilo o zrušitvi lahko posredujete po e-pošti. To nam bo v pomoč pri odpravi napak :)\n\nPriložite lahko tudi dodatno vsebino. Hvala! + Use the wysiwyg + When enabled, you will be able to format your text easily with tools. + Statistics + Total statuses + Number of boosts + Number of favourites + Number of mentions + Number of follows + Number of polls + Number of replies + Number of statuses + Statuses + Visibility + Number with media + Number with sensitive media + Number with CW + First status date + Last status date + First notification date + Last notification date + Frequency + %s statuses per day + %s notifications per day + Date range + Groups + No groups! + Disable custom animated emojis + Charts + Display charts + The application collects your local data, please wait… + Backup + Auto backup statuses + This option is per account. It will launch a service that will automatically store your statuses locally in the database. That allows to get statistics and charts + Auto backup notifications + This option is per account. It will launch a service that will automatically store your notifications locally in the database. That allows to get statistics and charts + Report account + Send an invitation + Your instance does not allow to register a new account! + + %d vote + %d votes + %d votes + %d votes + + + %d voter + %d voters + %d voters + %d voters + + + Single choice + Multiple choices + + + 5 minutes + 30 minutes + 1 hour + 6 hours + 1 day + 3 days + 7 days + + + Webview + Direct stream + + For joining my instance \"%1$s\", you can download Fedilab:\n\nF-Droid: %2$s\nGoogle: %3$s\n\nThen open the link below with Fedilab and create your account :)\n\n%4$s + + Your poll can\'t have duplicated options! + For all accounts + Database cache + Clear your home timeline cache + Clear your cached statuses + Clear your bookmarks + Files in cache + Total notifications + Skrij elemente menija + Fedilab is running live notifications + For %1$s accounts with %2$s events + Live notifications for %1$s + Live notifications will be enabled for this account. + Clear cache when leaving + The cache (media, cached messages, data from the built-in browser) will be automatically cleared when leaving the application. + Do you want to unfollow this account? + Show confirmation dialog before unfollowing + Replace Youtube with Invidio.us + Invidious is an alternative front-end to YouTube + Enter your custom host or leave blank for using invidious.snopyta.org + Replace Twitter with Nitter + Nitter is an open source alternative Twitter front-end focused on privacy. + Enter your custom host or leave blank for using nitter.net + Replace Instagram with Bibliogram + Bibliogram is an open source alternative Instagram front-end focused on privacy. + Enter your custom host or leave blank for using bibliogram.art + Replace Reddit with Libreddit + Libreddit is an open source alternative Reddit front-end focused on privacy. + Enter your custom host or leave blank for using libredd.it + Replace Medium links + Replace medium.com links with an open source alternative front-end focused on privacy. + Default: scribe.rip + Replace Wikipedia links + Replace Wikipedia link with an open source alternative front-end focused on privacy. + Default: wikiless.org + Hide Fedilab notification bar + For hiding the remaining notification in the status bar, tap on the eye icon button then uncheck: \"Display in status bar\" + Use a push notifications system for getting notifications in real time. + No live notifications + Live notifications + Notifications will be fetched every 15 minutes. + Add notes + Notes for the account + Allow to compress large photos into smaller sized photos with very less or negligible loss in quality of the image. + Allow compressing videos while maintaining their quality. + The app is compressing the media, it can be quite long… + Change app icon + Tap to change the app icon + Post + Visibility of the post + Tap here to add photos + Accepted Formats: jpeg, png, gif \n\nMax File Size: 15 MB \n\nAlbums can contain up to 4 photos or videos + Upload media + Add an optional caption + The app received a very long error message from the API %1$s + Message preview + Add mentions in each message + Fetching conversation + Order by + Title for the video + Join Peertube + I am at least 16 years old and agree to the %1$s of this instance + Links + Change the color of links (URLs, mentions, tags, etc.) in messages + Reblogs header + Change the color of display name at the top of messages + Change the color of the user name at the top of messages + Change the color of the header for reblogs + Posts + Background color of posts in timelines + Reset colors + Tap here to reset all your custom colors + Reset + Icons + Color of bottom icons in timelines + Pin this tag + Logo of the instance + Edit profile + Make an action + Translation + Image preview + Text color + Change the text color in pots + Apply changes + You need to restart the application to apply changes + Restart + Use a custom theme + Allow to override colors of the selected theme above + Theming + Store before + The theme was exported + The theme has been successfully exported in CSV + Apply the primary color to the status bar + Status bar color + Restore a default theme + Import a theme + Tap here to import a theme from a previous export + Export the theme + Tap here to export the current theme + An error occurred when selecting the theme file + Theme Picker + Select a pre-installed theme + Themes + Apply the primary color to the navigation bar + Navigation bar color + The underlying color of the app’s content. + Background color + Accents select parts of the UI. + Accent color + Displayed most frequently across your app. + Primary color + Export bookmarks to the instance + Import bookmarks from the instance + User count + Status count + Instance count + Blocked + End in %s + What\'s new in %s + You can follow my account for updates + This instance is not available on https://instances.social + Display full link + Share link + The URL has been copied to the clipboard + Open with another app + Check redirect + This URL does not redirect + %1$s \n\nredirects to\n\n %2$s + Change the user agent + Set a custom user agent or leave blank + Allows to customize the user agent used for api calls or with the built-in browser. + Remove UTM parameters + The app will automatically remove UTM parameters from URLs before visiting a link. + Trends + Trending now + %d people talking + Twitter accounts (via Nitter) + Twitter usernames space separated + Identity proofs + Verified identity + Verified by %1$s (%2$s) + Delete the notification + Display more options + It is a Pixelfed story + Upload a media, it will be automatically added to your Pixelfed story. + Media successfully added to your story! + Action disabled + Unfollow + Something went wrong, please check your download directory in settings. + Announcements + No announcements! + Add a reaction + Use your favourite browser inside the app. Uncheck this feature to open links externally. + Video cache in MB, zero means no cache. + Watermarks + Automatically add a watermark at the bottom of pictures. The text can be customized for each account. + No distributors found! + You need a distributor for receiving push notifications.\nYou will find more details at %1$s.\n\nYou can also disable push notifications in settings for ignoring that message. + Select a distributor + diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml new file mode 100644 index 000000000..632af5479 --- /dev/null +++ b/app/src/main/res/values-sr/strings.xml @@ -0,0 +1,1150 @@ + + + Отвори мени + Затвори мени + О програму + О инстанци + Приватност + Кеш + Одјава + Пријава + + Затвори + Да + Не + Поништи + Преузми + Преузми %1$s + Мултимедија сачувана + Фајл: %1$s + Лозинка + Е-пошта + Налози + Тутови + Ознаке + Сачувај + Поврати + Нема резултата! + Инстанца + Инстанца: mastodon.social + Сада сте на налогу %1$s + Додај налог + Садржај тута је копиран у клипборд + The URL of the toot has been copied to the clipboard + Измени + Одаберите слику… + Очисти + Камера + Избриши све + Преведи тут. + Закажи + Величине текста и иконица + Промените тренутну величину текста: + Промените тренутну величину иконица: + Следећа + Претходна + Отворите помоћу + Потврди + Мултимедија + Поделите са + Дељено преко Fedilab-а + Одговори + Корисничко име + Нацрти + Омиљени + Нови пратиоци + Помињања + Подршке + Прикажи подршке + Прикажи одговоре + Отвори у прегледачу + Преведи + Сачекајте неколико секунди пре него што урадите ову радњу. + + Почетна + Локална лајна + Здружена лајна + Подешавања + Омиљени + Комуникација + Ућуткани корисници + Блокирани корисници + Обавештења + Захтеви за праћење + Подешавања + Избриши налог + Избриши налог %1$s из апликације? + Пошаљи е-пошту + Кликните на путању да је промените + Неуспешно! + Заказани тутови + Информације испод не морају да тачно одсликавају кориснички профил. + Убаци емотикон + Апликација није покупила произвољне емотиконе за моменат. + Push notifications + Are you sure you want to logout? + Are you sure you want to logout @%1$s@%2$s? + + Нема тутова за приказ + No stories to display + Stories + Подржао (ла) %1$s + Додај овај тут у омиљене? + Уклони овај тут из омиљених? + Подржи овај тут? + Склони подршку за овај тут? + Прикачи овај тут? + Откачи овај тут? + Ућуткај + Блокирај + Пријави + Уклони + Копирај + Подели + Спомени + Временски ограничено ућуткивање + Избриши & и поново изради + + Ућуткај овај налог? + Блокирај овај налог? + Пријави овaj тут? + Блокирај овај домен? + Unmute this account? + Unblock this account? + + + Обавести + Нечујно + + + Уклони овај тут? + Избриши & и поново напиши овај тут? + + Забелешке + Додај у забелешке + Уклони забелешку + Нема забелешки за приказ + Статус је додат у забелешке! + Статус је уклоњен из забелешки! + + %d s + %d m + %d h + %d д + + %d second + %d seconds + %d seconds + + + %d minute + %d minutes + %d minutes + + + %d hour + %d hours + %d hours + + + %d day + %d days + %d days + + + Упозорење + Шта Вам је на уму? + Тутни! + QUEET! + cw + Напиши тут + Одговори на тут + Write a queet + Reply to a queet + Одаберите мултимедију + Грешка приликом одабирања мултимедије! + Уклони ову мултимедију? + Тут је празан! + Видљивост тута + Подразумевана видљивост тута: + Тут је послат! + Одговарате на овај тут: + Осетљив садржај? + + Пошаљи на јавне лајне + Не шаљи на јавне лајне + Пошаљи само пратиоцима + Пошаљи само поменутим корисницима + + Нема нацрта! + Одаберите тут + Одаберите налог + Одаберите неки налог + Уклони нацрт? + Кликните на дугме да прикажете оригинални тут + Опишите за слабовиде + + Нема описа! + + Издање %1$s + Програмер: + Лиценца: + GNU GPL V3 + Изворни код: + Превод тутова: + Претражи инстанце: + Дизајнер иконица: + + Разговор + + Нема налога за приказ + Нема захтева за праћење + Тутови \n %1$s + Праћени \n %1$s + Пратиоци \n %1$s + Прикачени \n %d + Одобри + Одбаци + + Нема заказаних тутова за приказ! + Напишите тут и потом одаберите Закажи из горњег менија. + Обриши заказани тут? + Мултимедија: %d + Тут је заказан! + Датум заказивања мора бити већи од тренутног сата! + Укључена је уштеда батерија! Ово можда и неће ради према очекивањима. + + Време за дужину ућуткивања треба да је веће од једног минута. + %1$s је ућуткан до %2$s.\n Можете искључити ућуткивање овом налогу са његове/њене профилне стране. + %1$s је ућуткан до %2$s.\n Кликните овде да искључите ућуткивање налогу. + + Нема обавештења за приказ + Вас је поменуо + wrote a new message + је подржао Ваш статус + је ставио Ваш статус као омиљени + Вас је запратио + asked to follow you + + и још једно обавештење + и још %d обавештења + и још %d обавештења + + + %d like + %d likes + %d likes + + Обриши обавештење? + Обрисати сва обавештења? + Обавештење је обрисано! + Сва обавештења су обрисана! + + Праћени + Пратиоци + Прикачени тутови + + Није могуће добити id клијента! + Unable to connect to instance domain! + Не постоји веза са интернетом! + Налог блокиран! + Налог није више блокиран! + Налог ућуткан! + Овај налог није више ућуткан! + Налог запраћен! + Налог се не прати више! + Тут је подржан! + Тут није више подржан! + Тут је додат у Ваше омиљене! + Тут је склоњен из Ваших омиљених! + Тут је пријављен! + Тут је обрисан! + Тут прикачен! + Тут није више прикачен! + Упс, дошло је до грешке! + Дошло је до грешке! Инстанца није вратила код за ауторизацију! + Не делује да је домен инстанце исправан! + Дошло је до грешке приликом пребацивања између налога! + Грешка приликом претраге! + Подаци о профилу су сачувани! + Није предузето ништа + Мултимедија је сачувана! + Десила се грешка приликом превођења! + Translations are disabled in settings + Нацрт сачуван! + Да ли сте сигурни да ова инстанца дозвољава оволики број карактера? Обично је ова вредност око 500 карактера. + Видљивост тутова је промењена за налог %1$s + + Број тутова по једном учитавању + Увек + Само на бежичној мрежи + Питај + Учитавање мултимедије + Учитавање слика + Прикажи више… + Прикажи мање… + Осетљив садржај + Онемогући GIF аватаре + Путања: + Аутоматски чувај нацрте + Додај адресу мултимедије у тутове + Обавести када Вас неко запрати + Обавести када неко подржи Ваш статус + Обавести када неко стави да му је Ваш статус омиљен + Обавести када Вас неко помене + Notify when a poll ended + Notify for new posts + Прикажи потврду пре давања подршке некоме + Прикажи дијалог за потврду пре додавања у омиљене + Обавештавај само на бежичној мрежи + Обавештења? + Тиха обавештења + NSFW временско ограничење приказа (у секундама, 0 значи искључено) + Media Description timeout (seconds, 0 means off) + Измени профил + Custom sharing + Your custom sharing URL… + Биографија… + Закључај налог + Сачувај промене + Одаберите слику заглавља + Прилагоди преглед слике да стане + Аутоматски подели тутове преко 500 карактера у одговору + Достигли сте дозвољених 160 карактера! + Достигли сте дозвољених 30 карактера! + Између + и + Време мора бити веће од %1$s + Време мора бити мање од %1$s + Почетно време + Крајње време + Користите уграђени прегледач + Произвољни језичци + Омогућите JavaScript + Аутоматски приказуј осетљиве садржаје + Дозволите независне колачиће + Ваш API кључ, можете оставити празно за Yandex + + Тамна + Светла + Црна + + Постави ЛЕД боју: + + Плава + Море плава + Магента + Зелена + Црвена + Жута + Бела + + Запрати + Одблокирај + Ућуткај + Искључи ућуткивање + Захтев послат + Прати Вас + Претрага + Прво слово велико у одговорима + Мењај величине слика + Resize videos + + Брза обавештења + Потврдите која брза обавештења желите да примате. + Можете укључити и искључити ова обавештења накнадно у подешавањима (језичак за Обавештења). + + + Обриши кеш + Тренутно има %1$s података у кешу.\n\nДа ли желите да их обришете? + Mb + Кеш очишћен! %1$s ослобођено + + Title + Title… + Description + Keywords + Keywords… + + Синхронизуј + Филтрирај + Ваши тутови + Your notifications + Јавни + Неизлистани + Приватни + Директни + Неке кључне речи… + Прикажи мултимедију + Прикажи закачене + Нема пронађених резултата! + Прављење резерве тутова за %1$s + %1$s нови тут је увежен + %1$s new notifications have been imported + + Dates descending + Dates ascending + + + Не + Само + Оба + + Нема тутова у бази. Користите дугме за синхронизацију да их преузмете. + + Снимљени подаци + Само основне информације о налозима су ускладиштене на уређају. + Ови подаци су строго поверљиви и може их користити само ова апликација. + Брисање апликација моментално брише ове податке.\n + ⚠ Пријаве и лозинке се никад не складиште. Користе се само за време безбедног (SSL) пријављивања са инстанцом. + + Дозволе: + - ACCESS_NETWORK_STATE: Користи се да види да ли је уређај накачен на бежичну мрежу.\n + - INTERNET: Користи се да прича са инстанцом.\n + - WRITE_EXTERNAL_STORAGE: Користи се да ускладишти мултимедију или да помери апликацију на SD картицу.\n + - READ_EXTERNAL_STORAGE: Користи се да дода мултимедију на тутове.\n + - BOOT_COMPLETED: Користи се да стартује сервис за обавештења.\n + - WAKE_LOCK: Користи се током рада сервиса за обавештења. + + API дозволе: + - Читај: Чита податке.\n + - Пиши: Шаље статусе и отпрема мултимедију за статусе.\n + - Прати: Прати, уклаља праћења, блокира, одблокира.\n\n + ⚠ Ове акције се изводе само када их корисник захтева. + + Праћење и библиотеке + Апликација не користи за праћење алатке > (мерење публике, грешке за извештавање, итд.) и не садржи никакве рекламе.\n\n + Употреба библиотека је минимизована: \n + - Glide: за управљање мултимедијом\n + - Android-Job: за управљање сервисима\n + - PhotoView: за управљање сликама\n + + Превод тутова + Апликација нуди могућност да преводи тутове на језик уређаја користећи Yandex API.\n + Yandex има своју конкретну политику приватности која се може наћи овде: https://yandex.ru/legal/confidential/?lang=en + + Хвала Вам на: + + Филтрирај по регуларним изразима + Претрага + Обриши + Дохвати још тутова… + + Листе + Да ли сте сигурни да желите да трајно избришете ову листу? + Тренутно нема ништа у овој листи. Када чланови ове листе пошаљу нове статусе, они ће се појавити овде. + Додај на листу + Додај листу + Обриши листу + Измени листу + Ново име листе + The account was added to the list! + You don\'t have any lists yet! + + %1$s се померио на %2$s + Пријављивање не ради? + Ево неких провера које могу да помогну:\n\n + - Проверите да нема грешака у куцању имена инстанце\n\n + - Проверите да ли је инстанца жива и ради\n\n + - Ако користите двофакторско пријављивање (2FA), користите линк на дну (када попуните име инстанце)\n\n + - Линк испод можете користити и без да користите 2FA\n\n + - Ако ништа ни даље не ради, пријавите нам проблем на Framagit-у, на https://framagit.org/tom79/fedilab/issues + + Мултимедија је учитана. Кликните овде да је прикажете. + Ова акција може да потраје. Бићете обавештени када се заврши. + Још увек се извршава, сачекајте… + Статуси извоза + Статуси извоза за %1$s + Извежено %1$s од укупно %2$s тутова. + Дошло је до грешке приликом извоза података за %1$s + Дошло је до грешке приликом извоза података! + Дошло је до грешке приликом увоза података! + + Прокси + Омогући прокси? + Домаћин + Порт + Пријава + Лозинка + Додај детаље тута приликом дељења + Подржи апликацију на Liberapay-у + Постоји грешка у регуларном изразу! + На овој инстанци није нађена ниједан временска лајна! + Избриши ову инстанцу? + Преведи на + Прати инстанцу + Већ пратите ову инстанцу! + Инстанца запраћена! + Партнерства + Информације + Сакриј подрше од %s + Истакнуто на профилу + Прикажи подршке од %s + Не истичи на профилу + Овај налог је сада истакнут на профилу + Овај налог није више истакнут на профилу + Подршке су сада приказане! + Подршке су сада скривене! + Директна порука + Филтери + Нема филтера за приказ. Можете га направити кликом на „+“ дугме. + Кључна реч или фраза + Локална лајна + Јавне лајне + Обавештења + Разговори + Биће поклопљено без обзира да ли су слова мала или велика или да је укључено упозорење за садржај на туту + Уклањај уместо сакривања + Филтрирани тутови ће нестати без могућности враћања, иако се филтер касније уклони + Када је кључна реч или фраза алфанумеричка, биће примењена само ако се поклапа са целом речју + Цела реч + Контекст филтера + Један или више контекста где треба да се примени филтер + Истиче после + Обриши филтер? + Ажурирај филтер + Направи филтер + Кога запратити + Тренутно нема излистаних налога! + Прати + Одабери све + Пониште све одабране + %s запраћен! + Правим списак %s + Додајем налоге на списак + Налози су додати на списак + Додајем налоге на списак + Још нисте направили ниједан списак. Кликните на \"+\" дугме да направите нови. + Кога пратити + Trunk API + Налог(зи) не могу бити запраћени + Дохватам удаљени налог + Аутоматски прошири скривену мултимедију + Нови пратиоц + Нова подршка + Нови омиљени + Ново спомињање + Poll Ended + Нови тут + Резервне копије тутова + New posts + Преузимање мултимедије + Промени звук обавештења + Изаберите тон + Омогући термин + Видео упутства + Дохватам удаљену преписку! + Нема блокираних домена! + Одблокирај домен + Да ли сте сигурни да желите да одблокирате %s? + Да ли сте сигурни да желите да блокирате %s? + Блокирани домени + Блокирај домен + Домен блокиран + Домен није више блокиран! + Дохватам удаљени статус + Коментар + Peertube инстанца + Први оставите коментар на овај видео са овим дугметом горе десно! + %s прегледа + Трајање: %s + Додај инстанцу + Коментари нису омогућени на овом видео запису! + Одаберите резолуцију + Peertube омиљени + Видео је додат у забелешке! + Видео је уклоњен из забелешки! + Нема Peertube видео записа у Вашим омиљеним ставкама! + Канал + Видео снимци + Канали + Користите Емоји 1 + Информације + Прикажи претпрегледе у свим тутовима + Нови дизајнер за UX/UI + Прикажи видео претпрегледе + Идентификација налога копирана у клипборд! + Промени језик + Подразумевани језик + Скратите дугачке тутове + Скратите тутове преко \'x\' линија. 0 значи да је искључено. + Прикажи још + Прикажи мање + Управљање ознакама + Ознака већ постоји! + Ознака ускладиштена! + Ознака промењена! + Ознака избрисана! + Закажи подршке + Подршка је заказана! + Нема заказаних подршки за приказ! + Закажи подршку.]]> + Уметничка лајна + Отвори мени + Go back + Логотип апликације + Профилна слика + Профилни банер + Контактирајте администратора ове инстанце + Додај нови + MastoHost лого + Емоџи бирач + Оcвежи + Прошири разговор + Уклони налог + Избриши блокирани домен + Произвољни емоџи бирач + Пусти видео + Нови тут + Слика картице + Сакриј мултимедију + Фавикон + Add description for media (for the visually impaired) + + Никадa + 30 минута + 1 сат + 6 сати + 12 сати + 1 дан + Недељу дана + + In this field, you need to write your instance host name.\nFor example, if you created your account on https://mastodon.social\nJust write mastodon.social (without https://)\n + You can start writing first letters and names will be suggested.\n\n + ⚠ The Login button will only work if the instance name is valid and the instance is up! + + Више информација + + Језици + Само мултимедија + Прикажи NSFW + Crowdin преводи + Crowdin менаџер + Превод апликације + О Crowdin-у + Бот + Pixelfed инстанца + Мастодонт инстанца + Било шта од овога + Све од овога + Ништа од овога + Било која од ових речи (одвојених белинама) + Све ове речи (одвојене белинама) + Add some words to filter (space-separated) + Измени име колоне + Misskey инстанца + No app supporting this link is installed on your device. + Претплате + Преглед + У порасту + Недавно додато + Локално + Отпреми + Одговори + Обриши коментар + Да ли сте сигурни да желите да обришете овај коментар? + Видео преко целог екрана + Режим за видео записе + Изабери фајл за отпремање + Моји видео снимци + Наслов + Лиценца + Категорија + Језик + This video contains mature or explicit content + Укључи коментаре на видео снимке + Ажурирај видео + Опис + Видео снимак је ажуриран! + Отпремање отказано! + Видео снимак је отпремљен! + Отпремам, сачекајте… + Кликните овде да измените видео податке. + Обриши видео + Да ли сте сигурни да желите да обришете овај видео снимак? + Приказуј NSFW видео снимке + Нема видео снимака за приказ! + Остави коментар + Подели + Одабери режим заказивања + Са уређаја + Са сервера + Тутови (Сервер) + Toots (Device) + Modify + Display new toots above the \"Fetch more\" button + Timelines + Interface + Contacts + %1$s commented your video %2$s]]> + %1$s is following your channel %2$s]]> + %1$s is following your account]]> + %1$s has been published]]> + %1$s succeeded]]> + %1$s failed]]> + %1$s published a new video: %2$s]]> + %1$s has been blacklisted]]> + %1$s has been unblacklisted]]> + Export data + Import Data + Select the file to import + An error occurred when selecting the backup file! + Add a public comment + Send comment + There is no Internet connection. Your message has been stored in drafts. + Plain text + HTML + Markdown + Logout account + All + Support the app + Open Collective enables groups to quickly set up a collective, raise funds and manage them transparently. + Copy link + Connect + Normal + Compact + Console + Set display mode + Patch the Security Provider + Update tracking domains + The tracking data base has been updated! + http calls blocked by the application + List of blocked calls + Submit + The data base has been exported! + Featured hashtags + Filter timeline with tags + No tags + Hide the \"delete\" button in the notification tab + Attach an image when sharing a URL + + Poll + Polls + Create a poll + Choice 1 + Choice 2 + Choice %d + You need two choices at least for the poll! + Done + end at %s + Refresh poll + Vote + A poll you have voted in has ended + A poll you tooted has ended + Customize + Categories + Time slot + Advanced + Display \'new\' badge on unread toots + Peertube + Move timeline + Hide timeline + Reorder timelines + List permanently deleted + Followed instance removed + Pinned tag removed + Undo + You need to keep two visible tabs! + Reorder timelines + Main timelines can only be hidden! + BBCode + Always mark media as sensitive + GNU instance + Cached status + Forward tags in replies + Long press to store media + Blur sensitive media + Display timelines in a list + Display timelines + Mark bot accounts in toots + Manage tags + Remember the position in Home timeline + History + Playlists + Display name + You don\'t have any playlists. Tap on the \"+\" icon to add a new playlist + You must provide a display name! + The channel is required when the playlist is public. + Create a playlist + There is nothing in this playlist yet. + redo + Gallery + Emoji + Sticker + Eraser + Text + Filter + Brush + Are you sure you want to exit without saving the image? + Discard + Saving… + Image Saved Successfully! + Failed to save Image + Opacity + Enable photo editor + Add a poll item + Remove last poll item + Mute conversation + Unmute conversation + The conversation is no longer muted! + The conversation is muted + Open application features + Timed mute + Mention the account + Refresh cache + Mention the status + News + General + Regional + Art + Journalism + Activism + Gaming + Technology + Adult content + Furry + Food + Logo of the instance + Something went wrong when checking available instances! + Join Mastodon + Choose an instance by picking up a category, then tap on a check button. + Choose an instance by tapping on a check button. + %1$s users + Confirm password + I agree to %1$s and %2$s + server rules + terms of service + Sign up + This instance works with invitations. Your account will need to be manually approved by an administrator before being usable. + Please, fill all the fields! + Passwords don\'t match! + The email doesn\'t seem to be valid! + Your username will be unique on %1$s + You will be sent a confirmation e-mail + Use at least 8 characters + Password should contain at least 8 characters + Username should only contain letters, numbers and underscores + Account created! + Your account has been created!\n\n + Think to validate your email within the 48 next hours.\n\n + You can now connect your account by writing %1$s in the first field and click on Connect.\n\n + Important: If your instance required validation, you will receive an email once it is validated! + + Save the message in drafts? + Administration + Reports + No reports to display! + Reconnect the account + The application failed to access the admin features. You might need to reconnect the account for having the correct scope. + Unresolved + Remote + Active + Pending + Disabled + Silenced + Suspended + Permissions + Email status + Login status + Joined + Most recent IP + Warn + Disable + Silence + Notify the user per e-mail + Custom warning + User + Moderator + Administrator + Confirmed + Not confirmed + Reported statuses + Account + Undo silence + Undo disable + Suspend + Undo suspend + The account is silenced! + The account is no longer silenced! + The account is suspended! + The account is no longer suspended! + The account is disabled! + The account is no longer disabled! + The account has been warned! + Display the admin menu + Display the admin feature in statuses + Allow + The account is approved! + The account is rejected! + Assign to me + Unassign + Mark as resolved + Mark as unresolved + Empty content! + Display Fedilab features button + The application needs to access audio recording + Voice message + Enable quick reply + The account you are replying might not see your message! + If disabled, the app will always load last statuses + If disabled, sensitive media will be hidden with a button + Store media in full size with a long press on previews + Add an ellipse button at the top right for listing all tags/instances/lists + During the time slot, the app will send notifications. You can reverse (ie: silent) this time slot with the right spinner. + Display a Fedilab button below profile picture. It is a shortcut for accessing in-app features. + Allow to reply directly in timelines below statuses + Previews will not be cropped in timelines + Allow to play embedded videos directly in timelines + Allow to reverse the way to read statuses that are displayed once tapping the fetch more button + This option allows to support recent cipher suites. It is useful for older Android devices or if you cannot connect to your instance. + Exclusively for Peertube videos. Switch this mode if you cannot play them. + These tags will allow to filter statuses from profiles. You will have to use the context menu for seeing them. + Automatically insert a line break after the mention to capitalize the first letter + Allow content creators to share statuses to their RSS feeds + Compose + Maximum retry times when uploading media + Create a new Folder here + Enter the folder name + Please enter a valid folder name + This folder already exists.\n Please provide another name for the folder + Select + Default Directory + Folder + Create folder + Display a toast message after an action has been completed (boost, fav, etc.)? + Muted instances have been exported! + Add an instance + Export instances + Import instances + Crash reports + Enable crash reports + If enabled, a crash report will be created locally and then you will be able to share it. + Масталаб је престао да ради :( + Можете ми послати извештај о грешци е-поштом. То може да ми помогне да је исправим:)\n\nМожете додати и неки додатни садржај. Хвала! + Use the wysiwyg + When enabled, you will be able to format your text easily with tools. + Statistics + Total statuses + Number of boosts + Number of favourites + Number of mentions + Number of follows + Number of polls + Number of replies + Number of statuses + Statuses + Visibility + Number with media + Number with sensitive media + Number with CW + First status date + Last status date + First notification date + Last notification date + Frequency + %s statuses per day + %s notifications per day + Date range + Groups + No groups! + Disable custom animated emojis + Charts + Display charts + The application collects your local data, please wait… + Backup + Auto backup statuses + This option is per account. It will launch a service that will automatically store your statuses locally in the database. That allows to get statistics and charts + Auto backup notifications + This option is per account. It will launch a service that will automatically store your notifications locally in the database. That allows to get statistics and charts + Report account + Send an invitation + Your instance does not allow to register a new account! + + %d vote + %d votes + %d votes + + + %d voter + %d voters + %d voters + + + Single choice + Multiple choices + + + 5 minutes + 30 minutes + 1 hour + 6 hours + 1 day + 3 days + 7 days + + + Webview + Direct stream + + For joining my instance \"%1$s\", you can download Fedilab:\n\nF-Droid: %2$s\nGoogle: %3$s\n\nThen open the link below with Fedilab and create your account :)\n\n%4$s + + Your poll can\'t have duplicated options! + For all accounts + Database cache + Clear your home timeline cache + Clear your cached statuses + Clear your bookmarks + Files in cache + Total notifications + Hide menu items + Fedilab is running live notifications + For %1$s accounts with %2$s events + Live notifications for %1$s + Live notifications will be enabled for this account. + Clear cache when leaving + The cache (media, cached messages, data from the built-in browser) will be automatically cleared when leaving the application. + Do you want to unfollow this account? + Show confirmation dialog before unfollowing + Replace Youtube with Invidio.us + Invidious is an alternative front-end to YouTube + Enter your custom host or leave blank for using invidious.snopyta.org + Replace Twitter with Nitter + Nitter is an open source alternative Twitter front-end focused on privacy. + Enter your custom host or leave blank for using nitter.net + Replace Instagram with Bibliogram + Bibliogram is an open source alternative Instagram front-end focused on privacy. + Enter your custom host or leave blank for using bibliogram.art + Replace Reddit with Libreddit + Libreddit is an open source alternative Reddit front-end focused on privacy. + Enter your custom host or leave blank for using libredd.it + Replace Medium links + Replace medium.com links with an open source alternative front-end focused on privacy. + Default: scribe.rip + Replace Wikipedia links + Replace Wikipedia link with an open source alternative front-end focused on privacy. + Default: wikiless.org + Hide Fedilab notification bar + For hiding the remaining notification in the status bar, tap on the eye icon button then uncheck: \"Display in status bar\" + Use a push notifications system for getting notifications in real time. + No live notifications + Live notifications + Notifications will be fetched every 15 minutes. + Add notes + Notes for the account + Allow to compress large photos into smaller sized photos with very less or negligible loss in quality of the image. + Allow compressing videos while maintaining their quality. + The app is compressing the media, it can be quite long… + Change app icon + Tap to change the app icon + Post + Visibility of the post + Tap here to add photos + Accepted Formats: jpeg, png, gif \n\nMax File Size: 15 MB \n\nAlbums can contain up to 4 photos or videos + Upload media + Add an optional caption + The app received a very long error message from the API %1$s + Message preview + Add mentions in each message + Fetching conversation + Order by + Title for the video + Join Peertube + I am at least 16 years old and agree to the %1$s of this instance + Links + Change the color of links (URLs, mentions, tags, etc.) in messages + Reblogs header + Change the color of display name at the top of messages + Change the color of the user name at the top of messages + Change the color of the header for reblogs + Posts + Background color of posts in timelines + Reset colors + Tap here to reset all your custom colors + Reset + Icons + Color of bottom icons in timelines + Pin this tag + Logo of the instance + Edit profile + Make an action + Translation + Image preview + Text color + Change the text color in pots + Apply changes + You need to restart the application to apply changes + Restart + Use a custom theme + Allow to override colors of the selected theme above + Theming + Store before + The theme was exported + The theme has been successfully exported in CSV + Apply the primary color to the status bar + Status bar color + Restore a default theme + Import a theme + Tap here to import a theme from a previous export + Export the theme + Tap here to export the current theme + An error occurred when selecting the theme file + Theme Picker + Select a pre-installed theme + Themes + Apply the primary color to the navigation bar + Navigation bar color + The underlying color of the app’s content. + Background color + Accents select parts of the UI. + Accent color + Displayed most frequently across your app. + Primary color + Export bookmarks to the instance + Import bookmarks from the instance + User count + Status count + Instance count + Blocked + End in %s + What\'s new in %s + You can follow my account for updates + This instance is not available on https://instances.social + Display full link + Share link + The URL has been copied to the clipboard + Open with another app + Check redirect + This URL does not redirect + %1$s \n\nredirects to\n\n %2$s + Change the user agent + Set a custom user agent or leave blank + Allows to customize the user agent used for api calls or with the built-in browser. + Remove UTM parameters + The app will automatically remove UTM parameters from URLs before visiting a link. + Trends + Trending now + %d people talking + Twitter accounts (via Nitter) + Twitter usernames space separated + Identity proofs + Verified identity + Verified by %1$s (%2$s) + Delete the notification + Display more options + It is a Pixelfed story + Upload a media, it will be automatically added to your Pixelfed story. + Media successfully added to your story! + Action disabled + Unfollow + Something went wrong, please check your download directory in settings. + Announcements + No announcements! + Add a reaction + Use your favourite browser inside the app. Uncheck this feature to open links externally. + Video cache in MB, zero means no cache. + Watermarks + Automatically add a watermark at the bottom of pictures. The text can be customized for each account. + No distributors found! + You need a distributor for receiving push notifications.\nYou will find more details at %1$s.\n\nYou can also disable push notifications in settings for ignoring that message. + Select a distributor + diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml new file mode 100644 index 000000000..5af30939a --- /dev/null +++ b/app/src/main/res/values-sv/strings.xml @@ -0,0 +1,1142 @@ + + + Öppna menyn + Stäng menyn + Om + Om instansen + Sekretess + Cache + Logga ut + Logga in + + Stäng + Ja + Nej + Avbryt + Ladda ned + Laddat ner %1$s + Media sparad + Fil: %1$s + Lösenord + E-post + Konton + Toots + Taggar + Spara + Återställ + Inga resultat! + Instans + Instans: mastodon.social + Fungerar nu med kontot %1$s + Lägg till ett konto + Tootens innehåll har kopierats till urklipp + URL i tooten har kopierats till urklipp + Ändra + Välj en bild… + Rensa + Kamera + Ta bort allt + Översätt denna toot. + Schemalägg + Storlek på text och ikoner + Ändra nuvarande textstorlek: + Ändra nuvarande ikonstorlek: + Nästa + Föregående + Öppna med + Bekräfta + Media + Dela med + Delat via Fedilab + Svar + Användarnamn + Utkast + Favoriter + Nya följare + Omnämningar + Boosts + Visa boostar + Visa svar + Öppna i webbläsare + Översätt + Vänligen vänta några sekunder innan du utför denna åtgärd. + + Hem + Lokal tidslinje + Federerad tidslinje + Alternativ + Favoriter + Kommunikation + Tystade användare + Blockerade användare + Notifieringar + Följförfrågningar + Inställningar + Ta bort ett konto + Radera kontot %1$s från programmet? + Skicka e-post + Klicka på sökvägen för att ändra den + Misslyckades! + Schemalagda toots + Informationen nedan kan avspegla användarens profil ofullständigt. + Infoga emoji + Appen kunde inte samla anpassade emojis för tillfället. + Push notifications + Är du säker på att du vill logga ut? + Är du säker på att du vill logga ut @%1$s@%2$s? + + Ingen toot att visa + Inga berättelser att visa + Berättelser + Knuffad av %1$s + Lägg till detta toot till dina favoriter? + Ta bort detta toot från dina favoriter? + Knuffa detta toot? + Ta bort knuff av denna toot? + Nåla denna toot? + Ta bort nål på denna toot? + Tysta + Blockera + Rapportera + Ta bort + Kopiera + Dela + Omnämn + Tidsinställd tystning + Ta bort & nytt utkast + + Tysta detta konto? + Blockera detta konto? + Anmäl den här tooten? + Blockera denna domän? + Återställ ljud på detta konto? + Unblock this account? + + + Meddela + Tyst + + + Ta bort denna toot? + Ta bort & skriv om denna toot? + + Bokmärken + Lägger till bokmärke + Ta bort bokmärke + Inga bokmärken att visa + Status har lagts till bokmärken! + Status togs bort från bokmärken! + + %d s + %d m + %d t + %d d + + %d sekund + %d sekunder + + + %d minut + %d minuter + + + %d timma + %d timmar + + + %d dag + %d dagar + + + Varning + Vad tänker du på? + TOOT! + QUEET! + cw + Skriv en toot + Svara på en toot + Skriv en queet + Svara på en queet + Välj en media + Ett fel uppstod när du valde media! + Ta bort denna media? + Din toot är tom! + Tootens synlighet + Synbarheten av tootar som standard: + Tooten har skickats! + Du svarar på denna toot: + Känsligt innehåll? + + Skicka till offentliga tidslinjer + Skickar inte till offentliga tidslinjer + Skicka enbart till följare + Skicka endast till nämnda användare + + Inga utkast! + Välj ett toot + Välj ett konto + Välj några konton + Ta bort utkast? + Klicka på knappen för att visa den ursprungliga tooten + Beskriv för synskadade + + Ingen beskrivning tillgänglig! + + Version %1$s + Utvecklare: + Licens: + GNU GPL V3 + Källkod: + Översättning av toots: + Sök instanser: + Ikondesigner: + + Konversation + + Inget konto att visa + Ingen följförfrågan + Toots \n %1$s + Följer \n %1$s + Följare \n %1$s + Fastnålad \n %d + Auktorisera + Avvisa + + Inga schemalagda toots att Visa! + Skriva en toot och välj sedan schema från den översta menyn. + Ta bort schemalagda toot? + Media: %d + Toot har schemalagts! + Planerat datum måste vara större än aktuell timme! + Batterisparfunktionen är aktiverad! Det kanske inte fungerar som förväntat. + + Tiden för muting bör vara större än en minut. + %1$s har stängts av tills %2$s. \n du kan slå på detta konto från dennes profilsida. + %1$s är avstängd tills %2$s. \n Klicka här för att slå på kontot. + + Ingen anmälan att Visa + nämnde dig + wrote a new message + ökat din status + favourited din status + följt dig + vill följa dig + + och en till notifikation + och %d till notifikationer + + + %d gillar + %d gillar + + Ta bort notifiering? + Radera alla notifikationer? + Notifieringen har tagits bort! + Alla notifieringar har tagits bort! + + Följer + Följare + Fastnålad + + Kan inte få tag på klientid! + Kan inte ansluta till instansdomänen! + Ingen internetuppkoppling! + Kontot är spärrat! + Kontot är inte längre spärrat! + Kontot är tystat! + Kontot är inte längre tystat! + Kontot följdes! + Kontot följs inte längre! + Tooten knuffades! + Tooten är inte längre knuffad! + Toot har lagts till dina favoriter! + Toot togs bort från dina favoriter! + Toot rapporterades! + Toot togs bort! + Tooten är fastnålad! + Nålen togs bort från tooten! + Oops! Ett fel uppstod! + Ett fel uppstod! Instansen returnerade inte en auktoriseringskod! + Domäninstansen verkar inte vara giltig! + Ett fel uppstod vid växling mellan konton! + Ett fel uppstod under sökningen! + Profildata har sparats! + Inga åtgärder kan vidtas + Inlägget har sparats! + Ett fel uppstod under översättningens gång! + Översättningar är inaktiverade i inställningarna + Utkast sparat! + Är du säker på att denna instans tillåter detta dessa antal tecken? Vanligtvis, är detta värde ungefär 500 tecken. + Synligheten för toots har ändrats för konto %1$s + + Antal toots per laddning + Alltid + WIFI + Fråga + Ladda media + Ladda bilderna + Visa mer… + Visa mindre… + Känsligt innehåll + Inaktivera GIF avatarer + Sökväg: + Spara utkast automatiskt + Lägga till URL för media i toots + Meddela när någon följer dig + Meddela när någon ökar din status + Meddela när någon favoriter din status + Meddela när någon nämner dig + Meddela när en röstning slutar + Notify for new posts + Visa en bekräftelsedialogruta före boostning + Visa bekräftelsedialogrutan innan du lägger till favoriter + Notifiera endast över WIFI + Notifiera? + Tysta aviseringar + NSFW visningspaus (i sekunder, 0 betyder av) + Media Beskrivnings-timeout (sekunder, 0 betyder avstängd) + Ändra profil + Anpassad delning + Din anpassade URL… + Bio… + Lås konto + Spara ändringar + Välj en omslagsbild + Passa förhandsgranskningen av bilder + Dela automatiskt toots med fler än 500 tecken i svar + Du har nått gränsen på max 160 tecken! + Du har nått gränsen på max 30 tecken! + Mellan + och + Intervallet måste vara större än %1$s + Intervallet måste vara lägre än %1$s + Starttid + Sluttid + Använd den inbyggda webbläsaren + Anpassade flikar + Aktivera Javascript + Utöka meddelanden automatiskt + Tillåt tredjepartskakor + Din API-nyckel, lämna blankt för Yandex + + Mörkt + Ljust + Svart + + Färg på LED: + + Blå + Cyan + Magenta + Grön + Röd + Gul + Vit + + Följ + Avblockera + Tysta + Avtysta + Begäran skickad + Följer dig + Sök + Första bokstaven stor i svar + Ändra storlek på bilder + Ändra storlek på video + + Push-meddelanden + Bekräfta push-meddelanden som du vill ta emot. + Du kan aktivera eller inaktivera dessa meddelanden senare i inställningar (fliken aviseringar). + + + Rensa cache + Det finns %1$s av data i cacheminnet.\n\nVill du rensa det? + MB + Cachen har rensats! %1$s frigjordes + + Titel + Titel… + Beskrivning + Nyckelord + Nyckelord… + + Synkronisera + Filter + Dina toots + Dina meddelanden + Offentligt + Ej listat + Privat + Direkt + Vissa nyckelord… + Visa media + Visa fastnålade + Inga matchande resultat hittades! + Säkerhetskopiering toots för %1$s + %1$s nya toots har importerats + %1$s nya notifieringar har importerats + + Fallande datum + Stigande datum + + + Nej + Endast + Båda + + Ingen toots hittades i databasen. Använd gärna knappen Synkronisera från menyn för att hämta dem. + + Sparad data + Endast grundläggande information från konton lagras på enheten. + Uppgifterna är konfidentiella och kan endast användas av programmet. + Borttagning av programmet tar omedelbart bort dessa data.\n + ⚠ Inloggning och lösenord lagras aldrig. De används endast under en säker autentisering (SSL) med en instans. + + Behörigheter: + - ACCESS_NETWORK_STATE: Används för att upptäcka om enheten är ansluten till ett trådlöst nätverk.\n + - INTERNET: Används för frågor till en instans.\n + - WRITE_EXTERNAL_STORAGE: Används för att lagra media eller flytta appen på ett SD-kort.\n + - READ_EXTERNAL_STORAGE: Används för att lägga till media till toots.\n + - BOOT_COMPLETED: Används för att starta meddelandetjänsten.\n + - WAKE_LOCK: Används under meddelandetjänsten. + + API-rättigheter: + - Read: Läs data.\n + - Write: Posta statusar och ladda upp media till statusar.\n + - Follow: Följ, av följ, blockera, avblockera.\n\n + ⚠ Dessa händelser sker endast när användaren begär det. + + Spårning och bibliotek + Applikationen använder inga tracking-verktyg (publikmätning, felrapportering etc.) och innehåller ingen reklam.\n\n + Användanded av tredje-parts bibliotek är minimerad: \n + - Glide: Till att hantera media\n + - Android-Job: Till att hantera services\n + - PhotoView: Till att hantera bilder\n + + Översättning av toots + Applikationen erbjuder möjligheten att översätta toots med hjälp av språket på enheten och Yandex API..\n + Yandex har sin integritetspolicy här: https://yandex.ru/legal/confidential/?lang=en + + Tack till: + + Filtrera ut av reguljära uttryck + Sök + Ta bort + Hämta fler toots… + + Listor + Är du säker du vill ta bort listan permanent? + Det finns inget i den här listan ännu. När medlemmar av denna lista skickar nya toots, visas de här. + Lägg till i lista + Lägg till lista + Ta bort lista + Redigera lista + Ny titel för lista + Kontot har lagts till i listan! + Du har inga listor ännu! + + %1$s har flyttats till %2$s + Autentisering fungerar inte? + Här är några kontroller som kan hjälpa:\n\n + - Kontrollera att det inte finns stavfel i instans namn\n\n + - Kontrollera att din instans inte är nere\n\n + - Om du använder tvåstegsfaktor autentisering (2FA), vänligen använd länken längst ner (när instansnamnet är ifyllt)\n\n + - Du kan också använda denna länk utan att använda 2FA\n\n + - Om det fortfarande inte fungerar, vänligen be om hjälp på Framagit https://framagit.org/tom79/fedilab/issues + + Media har laddats, klicka här för att visa. + Denna åtgärd kan ta ganska lång tid. Du kommer att meddelas när den är färdig. + Körs fortfarande, vänligen vänta… + Export toots + Exportera toots för %1$s + %1$s toots av %2$s har blivit exporterade. + Något gick fel vid exporteringen %1$s + Något gick fel vid exporteringen! + Något gick fel vid importeringen! + + Proxy + Aktivera proxy? + Värd + Port + Logga in + Lösenord + Lägg till detaljer om toot vid delning + Stöd appen på Liberapay + Det finns ett fel i det reguljära uttrycket! + Ingen tidslinje hittades på denna instans! + Ta bort denna instans? + Översätt till + Följ instans + Du följer redan denna instans! + Instansen följs! + Samarbete + Information + Dölj boosts för %s + Visa på profil + Visa boosts från %s + Visa inte på profil + Konto kommer nu visas på profilen + Konton visas inte längre på profilen + Boosta visas nu! + Boosts är nu dolt! + Direktmeddelande + Filter + Inga filter att visa. Du kan skapa ett genom att klicka på \"+\". + Nyckelord eller fras + Hemtidslinje + Publik tidslinje + Notifiering + Konversationer + Kommer att matchas oavsett gemener eller versaler i text eller innehåll som varnar för en toot + Släng istället för att dölja + Filtrerade toots försvinner irreversibelt, även om filtret tas bort + När sökord eller fras är alfanumeriska bara, kommer att det tillämpas endast om den matchar hela ordet + Hela ord + Filtrera sammanhang + En eller flera sammanhang där filtret ska gälla + Upphör efter + Ta bort filter? + Uppdatera filter + Skapa filter + Vem ska du följa + Det finns inga konton som visas för tillfället! + Följ + Markera allt + Avmarkera alla + %s följs! + Skapa lista %s + Lägger till konton i listan + Konton har lagts till i listan + Lägg till konton till listan + Du har inte skapat någon lista än. Klicka på \"+\" för att skapa en. + Vem ska du följa + Trunk API + Konto(n) kan inte följas + Hämtar fjärrkontot + Utöka automatiskt dold media + Nya följare + Ny boost + Ny favorit + Nytt omnämnande + Röstning avslutad + Ny Toot + Backup av Toot + New posts + Ladda ner media + Ändra notifieringsljud + Välj ton + Aktivera tidsfönster + Instruktionsfilmer + Hämtar extern tråd! + Inga blockerade domäner! + Avblockera domän + Är du säker att du vill avblockera %s? + Är du säker att du vill blockera %s?\n\nDu kommer inte se mer innehåll från denna domän i någon publiktidslinje eller i notifieringar. Dom du följer på denna domän kommer att tas bort. + Blockerade domäner + Blockera domän + Domänen är blockerad + Domänen blockeras ej längre! + Hämtar extern status + Kommentar + Peertube instans + Bli den första att lämna en kommentar på denna video genom knappen längst upp till höger! + %s visningar + Varaktighet: %s + Lägg till en instans + Kommentarer är inte påslaget på denna video! + Välj en upplösning + Peertubefavoriter + Filmen har lagts till bland bokmärken! + Filmen har tagits bort från bokmärken! + Det finns inga Peertube-filmer i dina favoriter! + Kanal + Videor + Kanal + Använd emoji One + Information + Visa förhandsvisning i alla toots + Ny UX/UI designer + Förhandsvisa filmer + Kontoid har kopierats! + Ändra språk + Standardspråk + Avkorta långa toots + Avkorta toot som är längre än \'x\' rader. Noll betyder avstängt. + Visa mer + Visa färre + Hantera taggar + Taggen finns redan! + Taggen har sparats! + Tagen har ändrats! + Taggen har tagits bort! + Schemalägg boost + Boosten har schemalagts! + Inga schemalagda boosts att visa! + Schemalägg boost.]]> + Konstidslinje + Öppna menyn + Gå tillbaka + Applikationens logo + Profilbild + Profil banner + Kontakta admin för instansen + Lägg till ny + MastoHost logotyp + Emoji-väljare + Uppdatera + Utöka konversation + Ta bort ett konto + Ta bort den blockerade domänen + Anpassad emoji-väljare + Spela film + Ny toot + Bild på kortet + Dölj media + Favicon + Media för att lägga till beskrivning + + Aldrig + 30 minuter + 1 timme + 6 timmar + 12 timmar + 1 dag + 1 vecka + + I det här fältet måste du skriva din instans värdnamn.\nTill exempel om du skapade ditt konto på https://mastodon.social\n skriv mastodon.social (utan https://)\n + Börja skriv de första bokstäverna, sen kommer förslag att visas.\n\n + ⚠ Inloggningsknappen fungerar bara om instansnamnet är giltig och instansen körs! + + Mer information + + Språk + Endast media + Visa NSFW + Öppna Crowdin-översättning + Crowdin ansvarig + Översätt applikationen + Om Crowdin + Bot + Pixelfed instans + Mastodon instans + Något av dessa + Alla dessa + Ingen av dem + Någon av dessa ord (mellanrumsseparerade) + Alla dessa ord (mellanrumsseparerade) + Lägg till några ord att filtrera (separera med mellanslag) + Ändra kolumnnamn + Misskey-instans + Ingen app finns installerad som kan hantera denna länk. + Prenumerationer + Översikt + Trendar + Nyligen tillagda + Lokal + Ladda upp + Svara + Ta bort kommentar + Är du säker på att ta bort denna kommentar? + Fullskärmsvideo + Lägen för video + Välj en fil som ska laddas upp + Mina videoklipp + Titel + Licens + Kategori + Språk + Den här videon innehåller moget eller explicit innehåll + Tillåt videokommentarer + Uppdatera video + Beskrivning + Videon har uppdaterats! + Uppladdning avbruten! + Videon har laddats upp! + Uppladdning pågår, var god vänta… + Klicka här för att editera videodata. + Ta bort film + Är du säker på att ta bort denna film? + Visa NSFW-filmer + Inga filmer att visa! + Lämna ett meddelande + Dela + Välj ett schema-läge + Från enhet + Från server + Toots (Server) + Toots (enhet) + Ändra + Visa nya toots ovanför knappen ”Hämta mer” + Tidslinjer + Gränssnitt + Kontakter + %1$s kommenterade din video %2$s]]> + %1$s följer din kanal %2$s]]> + %1$s följer ditt konto]]> + %1$s har publicerats]]> + %1$s, lyckades]]> + %1$s, misslyckades]]> + %1$s publicerade en ny video: %2$s]]> + %1$s har svartlistats]]> + %1$s, har tagits bort]]> + Exportera data + Importera data + Välj en fil att importera + Ett fel uppstod när säkerhetskopia valdes! + Lägg till en publik kommentar + Skicka kommentar + Det finns ingen Internetanslutning. Ditt meddelande har sparats i utkast. + Enbart text + HTML + Markdown + Logga ut konto + Allt + Stötta appen + Open Collective gör det möjligt för grupper att snabbt inrätta ett kollektiv, samla in pengar och hantera dem transparent. + Kopiera länk + Anslut + Nomal + Kompakt + Konsol + Ange visningsläge + Uppdatera säkerhetsleverantör + Uppdatera spårningsdomäner + Spårningsdatabasen har uppdaterats! + http-anrop blockerades av applikationen + Lista över blockerade anrop + Skicka + Databasen har exporterats! + Utvalda hashtags + Filtrera tidslinjen med taggar + Inga taggar + Dölj raderingsknappen på notifikationsfliken + Hämta metadata om webbadress delas av andra appar + + Enkät + Röstning + Skapa en enkät + Val 1 + Val 2 + Val %d + Du behöver minst två val för mätningen! + Klar + sluta på %s + Uppdatera enkät + Rösta + En enkät som du röstat i har avslutats + En enkät som du röstat i har avslutats + Anpassa + Kategorier + Tids lucka + Avancerad + Visa \"nytt\" märke på olästa tootar + Peertube + Flytta tidslinje + Dölj tidslinje + Ändra ordning på tidslinjer + Listan permanent borttagen + Följda instanser är borttagna + Fastnålad tagg borttagen + Ångra + Du måste behålla minst två synliga tabbar! + Ändra ordning på tidslinjer + Huvudtidslinjen kan bara döljas! + BBCode + Markera alltid media som känsligt + GNU-instans + Cachadstatus + Vidarebefordra taggar i svar + Långtryck för att spara media + Censurera känslig media + Visa tidslinjer i en lista + Visa tidslinjer + Markera robotkonton i toot + Hantera taggar + Kom ihåg positionen i hemtidslinjen + Historik + Spellistor + Visningsnamn + Du har inte några spellistor. Klicka på \"+\"-ikonen för att lägga till en ny spellista + Du måste ange ett visningsnamn! + Kanalen krävs när spellistan är offentliga. + Skapa spellista + Det finns ingenting i denna spellista ännu. + gör om + Galleri + Emoji + Dekal + Suddigummi + Text + Filter + Pensel + Är du säker på att du vill avsluta utan att spara bilden? + Kasta + Sparar… + Bilden har sparats! + Kunde inte spara bilden + Genomskinlighet + Aktivera bildeditor + Lägg till ett val + Ta bort det sista valet + Tysta konversationen + Sätt på konversationen + Konversationen är inte längre tystad! + Konversationen är tystad + Öppna applikationsfunktioner + Tidsinställd tystning + Nämn kontot + Uppdatera cache + Omnämn status + Nyheter + Allmänt + Regional + Konst + Journalistik + Aktivism + Gaming + Teknologi + Innehåll för vuxna + Päls + Mat + Logo för instansen + Något gick fel vid kontroll av tillgängliga instanser! + Gå med i Mastodon + Välj en instans genom att välja en kategori, klicka sedan på välj-knappen. + Välj en instans genom att markera kryssrutan. + %1$s användare + Bekräfta lösenord + Jag godkänner %1$s och %2$s + serverregler + tjänstevillkor + Registrera dig + Denna instans använder inbjudningar. Ditt konto måste godkännas manuellt av en administratör innan det kan användas. + Vänligen fyll i alla fält! + Lösenorden matchar inte! + E-posten verkar inte vara giltig! + Ditt användarnamn kommer att vara unik på %1$s + Du kommer att få en bekräftelse via e-post + Använda minst åtta (8) tecken + Lösenordet måste innehålla minst åtta (8) tecken + Användarnamn får endast innehålla bokstäver, siffror och understreck + Konto skapat! + Ditt konto har skapats!\n\n + Du måste bekräfta din e-post inom 48 timmar.\n\n + Anslut till ditt konto genom att skriva %1$s i det första fältet och klicka på Anslut.\n\n + Viktigt: Om din instans kräver validering så kommer du att få ett e-postmeddelande när det är validerad! + + Spara meddelandet i utkast? + Administration + Rapporter + Inga rapporter att visa! + Återanslut kontot + Programmet misslyckades med att ansluta administrationsfunktionen. Du måste kanske återansluta för att få rätt rättigheter. + Olösta + Distans + Aktiv + Väntande + Inaktiverad + Tystad + Avstängda + Rättigheter + E-poststatus + Inloggningsstatus + Medlemmar + Senaste IP + Varna + Inaktivera + Tyst + Meddela användaren per e-post + Anpassad varning + Användare + Moderator + Administratörer + Bekräftad + Ej bekräftad + Rapporterade status + Konto + Ångra tystnad + Ångra inaktivera + Avstängda + Ångra avstängda + Kontot är tystat! + Kontot är inte längre tystat! + Kontot är avstängt! + Kontot är inte längre avstängt! + Kontot är inaktiverat! + Kontot är inte inaktiverat! + Kontot har varnats! + Visa adminmenyn + Visa administrationsfunktion status + Tillåt + Kontot är godkänt! + Kontot har nekats! + Tilldela mig + Otilldelad + Markera som löst + Markera som olöst + Tomt innehåll! + Visa Fedilab funktioner-knapp + Programmet behöver tillgång till ljud-inspelning + Röstmeddelande + Aktivera snabbsvar + Det konto du svarar till kanske inte ser ditt meddelande! + Om inaktiverat så kommer programmet alltid att ladda senaste status + Om inaktiverad så kommer känsligt material att döljas med en knapp + Spara media i full upplösning genom att långtrycka på förhandsgranskningen + Lägg till en ellips-knapp längst upp till höger för att lista alla taggar/instanser/listor + Under tiden perioden kommer programmet att skicka meddelanden. Du kan ändra (dvs: tysta) denna period med höger spinner. + Visa en Fedilab-knapp nedan profilbilden. Det är en genväg för att få tillgång till in-app-funktioner. + Tillåt att svara direkt i tidslinjen nedanför statusen + Förhandsgranskning kommer inte att förkortas i tidslinjer + Tillåt att spela upp inbäddade videoklipp direkt i tidslinjer + Tillåt att vända sättet att läsa statusar som visas när du klickar på hämta fler-knappen + Detta alternativ gör det möjligt att stödja senaste chiffer-systemet. Det är användbart för äldre Android-enheter eller om du inte kan ansluta till din instans. + Exklusivt för Peertube videor. Växla till detta läge om du inte kan spela upp dem. + Dessa taggar gör det möjligt att filtrera status från profiler. Du måste använda innehållsmenyn för att se dom. + Lägg automatiskt till en radbrytning efter omnämnande för att sätta stor bokstav på första ordet + Tillåt innehållsskapare att dela statusar till deras RSS-flöden + Skapa + Maximalt antal försök vi uppladdning av media + Skapa en ny mapp här + Ange mappnamn + Ange ett giltigt mappnamn + Denna mapp finns redan.\n Ange ett annat namn för mappen + Välj + Standardmapp + Mapp + Skapa mapp + Visa toast-meddelande när en åtgärd har slutförts (boost, fav, etc.)? + Tystade instanser har exporterats! + Lägg till en instans + Exportera instanser + Importera instanser + Kraschrapport + Aktivera felrapporter + Om aktiverad kommer en krachrapport att skapas lokalt och du kan sen dela den. + Fedilab har stängts :( + Du kan skicka felrapport via e-post. Det kommer vara till hjälp för att åtgärda det :)\n\nDu kan också lägga till mer information. Tack så mycket! + Använd wysiwyg + När den är aktiverad, kommer du att kunna formatera din text lätt med verktyg. + Statistik + Totala status + Antal boosts + Antal favoriter + Antal omnämnanden + Antal följande + Antal omröstningar + Antal svar + Antal status + Statusar + Synlighet + Antalet med media + Antal med känslig media + Antal med CW + Datum för första statusen + Datum för senaste statusen + Första notifikationsdatum + Senaste notifikationsdatum + Frekvens + %s statusar per dag + %s notifikationer per dag + Datumintervall + Grupper + Inga grupper! + Inaktivera anpassad animerade emojis + Diagram + Visa diagram + Programmet samlar in din lokala data, vänligen vänta... + Säkerhetskopia + Status på automatisk säkerhetskopia + Det här alternativet är per konto. Det kommer starta en tjänst som sparar dina statusar lokalt i en databas. Detta kommer göra det möjligt att få ut statistik och diagram + Automatisk backup av notifikationer + Det här alternativet är per konto. Den kommer att starta en tjänst som sparar dina notifieringar lokalt i en databas. Detta kommer göra det möjligt att få ut statistik och diagram + Rapportera konto + Skicka inbjudan + Din instans tillåter inte nyregistreringar! + + %d röst + %d röster + + + %d röst + %d röster + + + Enkel val + Flervalsalternativ + + + 5 minuter + 30 minuter + 1 timme + 6 timmar + 1 dag + 3 dagar + 7 dagar + + + Webbvy + Direktström + + För att gå med i instansen \"%1$s\", så kan du ladda ner Fedilab:\n\nF-Droid:%2$s\nGoogle: %3$s\n\nSen öppna länken nedan med Fedilab och skapa ditt konto :)\n\n%4$s + + Din enkät kan inte ha duplicerade alternativ! + För alla konton + Databascache + Rensa din cache för hem-tidslinjen + Rensa dina cachade statusar + Rensa dina bokmärken + Filer i cacheminnet + Totalt antal notifieringar + Dölj menyobjekt + Fedilab kör live-notifieringar + För %1$s konton med %2$s händelser + Live-notifieringar för %1$s + Live-notifieringar kommer att inaktiveras endast för detta konto. + Rensa cacheminnet när du lämnar + Cach (media, cachade meddelanden, data från inbyggd browser) kommer rensas automatiskt när applikationen lämnas. + Vill du sluta följa detta konto? + Visa konfirmationsdialog innan avföljning + Ersätt Youtube med Invidio.us + Invidious är ett alternativt front-end till YouTube + Ange egen host eller lämna tomt för att använda invidio.us + Ersätt Twitter med Nitter + Nitter är en öppen källkods alternativ Twitter front-end fokuserat på integritet. + Ange din egen host eller lämna tomt för att använda nitter.net + Replace Instagram with Bibliogram + Bibliogram is an open source alternative Instagram front-end focused on privacy. + Enter your custom host or leave blank for using bibliogram.art + Replace Reddit with Libreddit + Libreddit is an open source alternative Reddit front-end focused on privacy. + Enter your custom host or leave blank for using libredd.it + Replace Medium links + Replace medium.com links with an open source alternative front-end focused on privacy. + Default: scribe.rip + Replace Wikipedia links + Replace Wikipedia link with an open source alternative front-end focused on privacy. + Default: wikiless.org + Dölj Fedilab meddelandefält + För att dölja kvarvarande notifieringar i statusbaren, klicka på ögonikons knappen och kryssa ur: \"Visa i statusbar\" + Use a push notifications system for getting notifications in real time. + Inge live-notifieringar + Live notifications + Notifieringar kommer att hämtas var 15:e minut. + Lätt till anteckning + Anteckningar för kontot + Tillåt att komprimera stora bilder till mindre bildstorlekar men väldigt liten eller ej synbar förlust av kvalité på bilden. + Tillåt komprimering av video och behåll kvalitén. + Appen komprimerar media, det kan ta en stund… + Ändra app-ikonen + Klicka här för att ändra app-ikonen + Posta + Synlighet för posten + Klicka här för att lägga till foton + Accepterade format: jpeg, png, gif \n\nMax filstorlek: 15 MB \n\nAlbum kan ha upp till 4 foton eller videor + Ladda upp media + Lägg till en valfri beskrivning + Appen mottog ett väldigt långt felmeddelande från API:et %1$s + Förhandsvisning av meddelande + Ange nämnande i varje meddelande + Hämtar konversation + Sortera efter + Titel på videon + Gå med i Peertube + Jag är minst 16 år gammal och godkänner %1$s för denna instans + Länkar + Ändra färg på länkar (Url:er, omnämnande, taggar, etc.) i meddelanden + Ompublicera-rubrik + Ändra färg på visningsnamn på toppen av meddelanden + Ändra färg på användarens namn i toppen av meddelanden + Ändra färgen för rubriken för ompublicera + Inlägg + Bakgrundsfärg för poster i tidslinjen + Återställ färger + Klicka här för att återställa alla dina egna färger + Återställ + Ikoner + Färg på bottenikoner i tidslinjer + Pinna denna tagg + Logo för instansen + Ändra profil + Gör en handling + Översättning + Förhandsvisning + Textfärg + Ändrar färgen på meddelanden + Tillämpa ändringar + Du måste starta om programmet för att tillämpa ändringarna + Starta om + Använd ett anpassat tema + Tillåt att åsidosätta färger på valt tema ovan + Tema + Spara innan + Temat har exporterats + Temat har exporteras till CSV + Använd den primära färgen till statusraden + Färg på statusrad + Återställ standardtema + Importera tema + Tryck här för att importera tema från tidigare export + Exportera tema + Tryck här för att exportera nuvarande tema + Ett fel uppstod när tema valdes + Tema väljare + Välj ett förinstallerat tema + Tema + Använd den primära färgen för navigationsfältet + Navigationsfältets färg + Den underliggande färgen av appens innehåll. + Bakgrundsfärg + Accenter välj delar av användargränssnittet. + Accentfärg + Visa \"oftast\" i din app. + Primärfärg + Exportera bokmärken till instans + Importera bokmärken från instans + Antal användare + Status, antal + Instanser, antal + Blockerad + Slutar om %s + Vad är nytt i %s + Du kan följa mitt konto för uppdateringar + Denna instans är inte tillgänglig på https://instances.social + Visa fullständig länk + Dela länk + URL:en har kopierats till urklipp + Öppna med en annan app + Kontrollera omdirigering + Denna URL kan inte omdirigera + %1$s \n\nomdirigerar till\n\n %2$s + Ändra användaragenten + Ange en anpassad användaragent eller lämna tomt + Gör det möjligt att anpassa den användaragent som används för api-anrop, eller med den inbyggda webbläsaren. + Ta bort UTM parametrar + Appen kommer automatiskt ta bort UTM-parametrar från webbadresser innan du besöker en länk. + Trender + Trendar nu + %d personer pratar + Twitter-konton (via Nitter) + Twitteranvändarnamn separerade med mellanslag + Identitetsbevis + Verifiera identitet + Verifierad av %1$s (%2$s) + Ta bort notifiering + Visa fler alternativ + Det är en Pixelfed story + Ladda upp media, det kommer automatiskt att läggas till på din Pixelfed story. + Media har lagts till i din berättelse! + Åtgärd inaktiverad + Sluta följa + Något gick fel, kontrollera din nedladdningskatalog i inställningarna. + Meddelanden + Inga meddelanden! + Lägg till en reaktion + Använd din favorit webbläsare i appen. Avmarkera denna funktion för att öppna länkar externt. + Video cache in MB, zero means no cache. + Watermarks + Automatically add a watermark at the bottom of pictures. The text can be customized for each account. + No distributors found! + You need a distributor for receiving push notifications.\nYou will find more details at %1$s.\n\nYou can also disable push notifications in settings for ignoring that message. + Select a distributor + diff --git a/app/src/main/res/values-szl/strings.xml b/app/src/main/res/values-szl/strings.xml new file mode 100644 index 000000000..b331afc04 --- /dev/null +++ b/app/src/main/res/values-szl/strings.xml @@ -0,0 +1,1156 @@ + + + Ôtwōrz myni + Zawrzij myni + Informacyje + Ô instancyji + Prywatność + Pamiyńć cache + Ôdloguj + Wloguj + + Zawrzij + Ja + Niy + Pociep + Ściōng + Ściōng %1$s + Zbiory spamiyntane + Zbiōr: %1$s + Hasło + Email + Kōnta + Tuty + Tagi + Spamiyntej + Prziwrōć + Żodnych wynikōw! + Instancyjo + Instancyjo: mastodon.social + Teroz je używane kōnto %1$s + Dodej kōnto + Zawartość tego tuta była skopiowano do skrytki + Adresa URL tego tuta była skopiowano do skrytki + Umiyń + Wybier ôbrozek… + Wysnoż + Fotoaparat + Wymaż wszyjsko + Przełōż tyn tut. + Zaplanuj + Srogość ikōn i tekstu + Umiyń srogość teroźnego tekstu: + Umiyń srogość teroźnyj ikōny: + Dalsze + Poprzednie + Ôtwōrz we + Potwiyrdź + Media + Podziel sie ze + Udostympniōne bez Fedilab + Ôdpowiedzi + Miano ôd używocza + Kōncepty + Ôblubiōne + Nowi śledzōncy + Spōmniynia + Rozgłoszynia + Pokoż rozgłoszynia + Pokoż ôdpowiedzi + Ôtwōrz we przeglōndarce + Przełōż + Poczekej pora sekund przed tōm akcyjōm. + + Przodek + Lokalno ôś czasu + Federowano ôś czasu + Ôpcyje + Ôblubiōne + Kōmunikacyjo + Wyciszyni + Zablokowani + Powiadōmiynia + Prośby ô śledzynie + Sztelōnki + Wymaż kōnto + Wymazać kōnto %1$s ze aplikacyje? + Wyślij bez email + Tyknij ściyżki, żeby ja zmiynić + Feler! + Zaplanowane tuty + Informacyjo niżyj może pokazować niypołny profil ôd używocza. + Wraź emoji + Aplikacyjo na razie niy zebrała ekstra emoji. + Push notifications + Na zicher chcesz sie ôdlogować? + Na zicher chcesz ôdlogować @%1$s@%2$s? + + Żodnych tutōw do pokozanio + Żodnych historyji do pokozanio + Historyje + Rozgłoszōne ôd %1$s + Przidać tyn tut do ôblubiōnych? + Wymazać tyn tut ze ôblubiōnych? + Rozgłosić tyn tut? + Niy rozgłoszać tego tuta? + Przipnōńć tyn tut? + Ôdepnōńć tyn tut? + Wycisz + Zablokuj + Zgłoś + Wymaż + Kopiuj + Podziel sie + Spōmnij + Czasowe wyciszynie + Wymaż i przeredaguj + + Wyciszyć to kōnto? + Zablokować to kōnto? + Zgłosić tyn tut? + Zablokować ta dōmyna? + Niy wyciszać już tego kōnta? + Niy blokować już tego kōnta? + + + Powiadōm + Cisza + + + Wymazać tyn tut? + Wymazać i przeredagować tyn tut? + + Zokłodki + Przidej do zokłodek + Wymaż zokłodka + Żodnych zokłodek do pokozanio + Tut bōł przidany do zokłodek! + Tut bōł wymazany ze zokłodek! + + %d s + %d min + %d godz. + %d d + + %d sekunda + %d sekundy + %d sekund + %d sekundy + + + %d minuta + %d minuty + %d minut + %d minuty + + + %d godzina + %d godziny + %d godzin + %d godziny + + + %d dziyń + %d dni + %d dni + %d dnia + + + Pozōr + Co ci chodzi po gowie? + Tutnij + Wyślij + cw + Napisz tut + Ôdpowiydz na tut + Napisz kwit + Ôdpowiydz na kwit + Wybier media + Doszło do błyndu przi wybiyraniu medium! + Wymazać to medium? + Twōj tut je prōzny! + Widzialność tuta + Wychodno widzialność tutōw: + Tut bōł wysłany! + Ôdpowiadosz na tyn tut: + Uraźliwo treść? + + Pokazuj na publicznych ôsiach czasu + Niy pokazuj na publicznych ôsiach czasu + Pokazuj ino śledzōncym + Pokazuj ino spōmnianym + + Żodnych kōnceptōw! + Wybier tut + Wybier kōnto + Wybier jakeś kōnta + Wymazać kōncept? + Tyknij knefel, żeby pokozać ôryginalny tut + Ôpis do słabowidzōncych + + Ôpis niydostympny! + + Wersyjo %1$s + Programista: + Licyncyjo: + GNU GPL V3 + Kod zdrzōdłowy: + Przekłod tutōw: + Szukej instancyji: + Projekt ikōn: + + Kōnwersacyjo + + Żodnych kōnt do pokozanio + Żodnych prōśb ô śledzynie + Tuty \n %1$s + Śledzi \n %1$s + Śledzōm \n %1$s + Przipniynte \n %d + Przizwōl + Ôdciep + + Żodnych zaplanowanych tutōw na dziś! + Napisz tut i wybier Zaplanuj ze wiyrchnigo myni. + Wymazać zaplanowany tut? + Media: %d + Tut je zaplanowany! + Zaplanowano data musi być niyskorzij niż teroz! + Szanowanie bateryje je aktywne! Może niy robić podle ôczekowań. + + Czas wyciszynio musi być dugszy niż jedna minuta. + Kōnto %1$s je wyciszōne do %2$s.\n Możesz to ôdkozać na jego profilu. + Kōnto %1$s je wyciszōne do %2$s.\n Tyknij sam, żeby to ôdkozać. + + Żodnych powiadōmiyń do pokozanio + spōmino cie + wrote a new message + rozgłoszo twōj status + lubi twōj status + śledzi cie + pyto ô śledzynie + + i inksze powiadōmiynie + i %d inksze powiadōmiynia + i %d inkszych powiadōmiyń + i %d inkszego powiadōmiynio + + + %d lubi + %d lubi + %d lubi + %d lubi + + Wymazać powiadōmiynie? + Wymazać wszyjske powiadōmiynia? + Powiadōmiynie było wymazane! + Wszyjske powiadōmiynia były wymazane! + + Śledzi + Śledzōm + Przipniynte + + Niy idzie dostać klijynckigo id! + Niy idzie sie skuplować ze dōmynōm instancyje! + Niy ma połōnczynio ze internetym! + Kōnto było zablokowane! + To kōnto już niy je blokowane! + Kōnto było wyciszōne! + Kōnto już niy je wyciszōne! + Kōnto je śledzōne! + Kōnto już niy je śledzōne! + Tut bōł rozgłoszōny! + Tut już niy je rozgłoszany! + Tut bōł przidany do ôblubiōnych! + Tut bōł wymazany ze ôblubiōnych! + Tut bōł zgłoszōny! + Tut bōł wymazany! + Tut bōł przipniynty! + Tut bōł ôdepniynty! + Doszło do błyndu! + Doszło do błyndu! Instancyjo niy swrōciyła kodu autoryzacyje! + Dōmyna instancyje niy wyglōndo na noleżno! + Doszło do błyndu przi szaltrowaniu miyndzy kōntami! + Doszło do błyndu przi szukaniu! + Dane profilu były spamiyntane! + Nic niy idzie zrobić + Zbiōr spamiyntany! + Doszło do błyndu przi przekłodaniu! + Przekłady sōm zastawiōne we sztelōnkach + Kōncept spamiyntany! + Na zicher ta instancyjo zwolo na tyla znakōw? Nojczyńścij to je kole 500. + Widzialność tutōw była zmiyniōno dlo kōnta %1$s + + Liczba tutōw na wgranie + Dycki + Wifi + Pytej + Ladowanie mediōw + Laduj ôbrozki + Pokoż wiyncyj… + Pokoż mynij… + Uraźliwo treść + Zastow awatary GIF + Ściyżka: + Autōmatycznie spamiyntuj kōncepty + Przidej URL mediōw we tutach + Powiadōm, jak ftoś mie biere śledzić + Powiadōm, jak ftoś rozgłoszo mōj status + Powiadōm, jak ftoś lubi mōj status + Powiadōm, jak ftoś mie spōmino + Powiadōm, jak anketa sie skōńczy + Notify for new posts + Pytej ô potwiyrdzynie przed rozgłoszyniym + Pytej ô potwiyrdzynie przed polubiyniym + Powiadōm ino przi Wifi + Powiadōmiać? + Ciche powiadōmiynia + Czas pokazowanio materyji uraźliwych (sekundy, 0 znaczy zastawiōne) + Limit czasu ôpisu mediōw (sekundy, 0 znaczy zastawiōny) + Edytuj profil + Włosne udostympnianie + Twōj URL włosnego udostympnianio… + Biografijo… + Zablokuj kōnto + Spamiyntej zmiany + Wybier wiyrchni ôbrozek + Pokoż cołke ôbrozki + Autōmatycznie dziel tuty we ôdpowiedziach, jak skōnczy sie limit znakōw: + Limit 160 znakōw bōł przekroczōny! + Limit 30 znakōw bōł przekroczōny! + Miyndzy + a + Czas musi być niyskorzij niż %1$s + Czas musi być wcześnij niż %1$s + Poczōntek + Kōniec + Używej wbudowanyj przeglōndarki + Włosne karty + Włōncz Javascript + Autōmatycznie pokoż cw + Zwolo na zewnyntrzne cookie + Twōj klucz API, przi Yandex idzie ôstawić prōzne + + Ciymny + Jasny + Czorny + + Nasztaluj farba LED: + + Modro + Cyjan + Magynta + Zielōno + Czerwōno + Żōłto + Bioło + + Śledź + Ôdblokuj + Wycisz + Niy wyciszej + Prośba wysłano + Śledzi cie + Szukej + Ôd srogij litery we ôdpowiedziach + Zmiyń srogość ôbrozkōw + Zmiyń srogość filmōw + + Powiadōmiynia push + Skoż, kere powiadōmiynia chcesz dostować. + Możesz te powiadōmiynia włōnczyć abo zastawiać niyskorzij we sztelōnkach (karta Powiadōmiynia). + + + Ôprōznij cache + Je %1$s danych we cache.\n\nChcesz je wymazać? + Mb + Cache ôprōzniōne! %1$s zwolniōne + + Tytuł + Tytuł… + Ôpis + Słowa kluczowe + Słowa kluczowe… + + Synchrōnizuj + Filtrowanie + Twoje tuty + Twoje powiadōmiynia + Publiczne + Niypubliczne + Prywatne + Bezpostrzednie + Pora słōw kluczowych… + Pokoż zbiōr + Pokoż przipniynte + Żodne wyniki tymu niy pasujōm! + Kopijo ibryczno tutōw ôd %1$s + %1$s nowych tutōw było zaimportowanych + %1$s nowych powiadōmiyń było zaimportowanych + + Datami do zadku + Datami do przodku + + + Niy + Ino + Ôba + + Żodnych tutōw niy szło znojś we bazie danych. Tyknij knefla synchrōnizacyje we myni, żeby je ściōngnōńć. + + Trzimane dane + Ino bazowe informacyje ze kōnt sōm trzimane na maszinie. + Te dane sōm blank tajne i może je używać ino aplikacyjo. + Skasowanie aplikacyje zaroz wymazuje tyż dane.\n + ⚠ Login i hasła nigdy niy sōm trzimane. Sōm ino używane w czasie bezpiecznyj autoryzacyje (SSL) we instancyji. + + Uprawniynia: + - ACCESS_NETWORK_STATE: Używane do wykrywanio, czy maszina je skuplowano ze necym Wifi.\n + - INTERNET: Używane do pytań do instancyje.\n + - WRITE_EXTERNAL_STORAGE: Używane do trzimanio mediōw abo przeniesiynio aplikacyje na karta SD.\n + - READ_EXTERNAL_STORAGE: Używane do przidowanio mediōw do tutōw.\n + - BOOT_COMPLETED: Używane to aktywowanio usugi powiadōmiyń.\n + - WAKE_LOCK: Używane przi usudze powiadōmiyń. + + Uprawniynia API: + - Czytej: Czytej dane.\n + - Spamiyntuj: Postuj statusy i wgrowej media dō nich.\n + - Śledź: Śledź, niy śledź, blokuj, niy blokuj.\n\n + ⚠ Te akcyje sōm kludzōne ino jak uczywocz ich zażōndo. + + Śledzynie i bibliotyki + Aplikacyjo niy używo noczyń, co śledzōm (mierzynie publiczności, reportowanie felerōw, etc.) i niy ma w nij żodnych reklam.\n\n + Je minimalne użycie bibliotyk: \n + - Glide: Sprawio media\n + - Android-Job: Sprawio usugi\n + - PhotoView: Sprawio ôbrozki\n + + Przekłod tutōw + Aplikacyjo dowo możliwość przekładu tutōw ze użyciym locale masziny i API Yandeksu.\n + Yandex mo polityka prywatności, co jōm idzie znojś sam: https://yandex.ru/legal/confidential/?lang=en + + Podziynkowania: + + Filtruj bez regularne wyrażynia + Szukej + Wymaż + Ściōng wiyncyj tutōw… + + Listy + Na zicher chcesz doimyntnie wymazać ta lista? + Jeszcze nic niy ma na tyj liście. Jak jeji czōnkowie ôpublikujōm nowe statusy, to ône sie pokożōm sam. + Przidej do listy + Przidej lista + Wymaż lista + Edytuj lista + Nowy tytuł listy + Kōnto było dodane do listy! + Jeszcze niy mosz żodnych list! + + Kōnto %1$s przeniysiōne do %2$s + Autoryzacyjo niy funguje? + Tukej pora dorad, co mogōm pōmōc:\n\n + - Wejzdrzij, czy niy ma literōwek we mianie instancyje\n\n + - Wejzdrzij, czy twoja instancyjo funguje\n\n + - Jeźli używosz dwuetapowego logowanio (2FA), to użyj linka na spodku (jak już wkludzisz miano instancyje)\n\n + - Możesz tyż użyć tego linka bez 2FA\n\n + - Jak to durch niy pōmoże, to ôtwōrz problym na FramaGit na https://framagit.org/tom79/fedilab/issues + + Medium zaladowane. Tyknij sam, żeby je ôbejzdrzeć. + Ta akcyjo może być fest dugo. Dostaniesz powiadōmiynie, jak bydzie gotowo. + Durch pracuje, czekej… + Eksportuj statusy + Eksportuj statusy ôd %1$s + %1$s tutōw ze %2$s było wyeksportowanych. + Coś poszło źle przi eksportowaniu danych ôd %1$s + Coś poszło źle przi eksportowaniu danych! + Coś poszło źle przi importowaniu danych! + + Proxy + Włōnczyć proxy? + Host + Port + Login + Hasło + Przidej informacyje ô tucie przi udostympnianiu + Spiyrej aplikacyjo na Liberapay + Je feler we wyrażyniu regularnym! + Niy szło znojś żodnych raji na tyj instancyji! + Wymazać ta instancyjo? + Przełōż na + Śledź instancyjo + Już śledzisz ta instancyjo! + Ta instancyjo je śledzōno! + Partnerstwa + Informacyjo + Skryj rozgłosy ôd %s + Polecej na profilu + Pokazuj rozgłosy ôd %s + Niy polecej na profilu + Kōnto je teroz polecane na profilu + Kōnto już niy je polecane na profilu + Rozgłosy sōm teroz pokazowane! + Rozgłosy sōm teroz kryte! + Bezpostrzednio wiadōmość + Filtry + Żodnych filtrōw do pokozanio. Możesz jakiś stworzić tykniyńciym we knefel „+”. + Słowo kluczowe abo fraza + Dōmowo raja + Publiczne raje + Powiadōmiynia + Godki + Srogość liter we tekście ani we upozorniyniu ô zawartości niy majōm znaczynio + Ôdciep zamiast kryć + Filtrowane tuty zniknōm doimyntnie, nawet jak filter bydzie wymazany + Jak słowo kluczowe abo fraza sōm ino alfanumeryczne, to to bydzie użyte, jak bydzie pasować cołke słowo + Cołke słowo + Kōnteksty filtra + Jedyn abo wiyncyj kōntekstōw, kaj filter winiyn być użyty + Zastow po + Wymazać filter? + Aktualizuj filter + Stwōrz filter + Kogo śledzić + Niy ma teroz kōnt na liście! + Śledź + Zaznacz wszyjsko + Ôdznacz wszyjsko + Kōnto %s je śledzōne! + Tworzynie listy %s + Przidowanie kōnt do listy + Kōnta były przidane do listy + Przidowanie kōnt do listy + Niy mosz jeszcze żodnyj listy. Tyknij knefel „+” i dodej nowo. + Kogo śledzić + Trunk API + Kōnt(a) niy idzie śledzić + Ściōnganie zdalnego kōnta + Autōmatycznie pokazuj skryte media + Nowe śledzynie + Nowy rozgłos + Nowe ôblubiynie + Nowe spōmniynie + Anketa skōńczōno + Nowy tut + Kopijo ibryczno tutōw + New posts + Ściōnganie mediōw + Zmiyń klang powiadōmiyń + Wybier tōn + Aktywuj przedzioł czasowy + Wideoinstrukcyje + Ściōnganie zdalnyj godki! + Żodnych zablokowanych dōmyn! + Ôdblokuj dōmyna + Na zicher ôdblokować %s? + Na zicher zablokować %s?\n\nNiy bydziesz widzieć nic z tyj dōmyny na żodnyj publicznyj raji ani we powiadōmiyniach. Twoji śledzōncy ze tyj dōmyny bydōm ôdłōnczyni. + Zablokowane dōmyny + Zablokuj dōmyna + Dōmyna je zablokowano + Dōmyna już niy je blokowano! + Ściōnganie zdalnego statusu + Skōmyntuj + Instancyjo Peertube + Bydź piyrszo ôsoba, co ôstawi kōmyntorz pod tym filmym. Tyknij knefla na wiyrchu z prawyj! + %s ôbejzdrzyń + Dugość: %s + Przidej instancyjo + Tyn film niy przizwolo na kōmyntorze! + Wybier rozdzielczość + Ôblubiōne Peertube + Film bōł dodany do zokłodek! + Film bōł wymazany ze zokłodek! + Niy ma filmōw Peertube we ôblubiōnych! + Kanał + Filmy + Kanały + Użyj Emoji One + Informacyjo + Pokazuj podglōnd we wszyjskich tutach + Nowy projektant UX/UI + Pokazuj podglōndy wideo + Id kōnta było skopiowane do skrytki! + Zmiyń jynzyk + Wychodny jynzyk + Przitnij duge tuty + Przitnij tuty dugsze jak \'x\' liniji. Zero ôznaczo zastawiynie. + Pokoż wiyncyj + Pokoż mynij + Sprawuj tagi + Tag już istniyje! + Tag bōł spamiyntany! + Tag bōł zmiyniōny! + Tag bōł wymazany! + Zaplanuj rozgłos + Rozgłos zaplanowany! + Żodnych zaplanowanych rozgłosōw! + Zaplanuj rozgłos.]]> + Raja kōnsztu + Ôtwōrz myni + Nazod + Logo aplikacyje + Profilowy ôbrozek + Profilowy baner + Kōntakt ze administratorym instancyje + Przidej nowy + Logo MastoHost + Wybōr emoji + Ôdświyż + Rozwiń godka + Wymaż kōnto + Wymaż zablokowano dōmyna + Włosny wybōr emoji + Pokoż film + Nowy tut + Ôbrozek przi karcie + Skryj media + Favicon + Przidej ôpis do medium (do słabowidzōncych) + + Nigdy + 30 minut + 1 godzina + 6 godzin + 12 godzin + 1 dziyń + 1 tydziyń + + W tym polu trzeba wkludzić dōmyna twojij instancyje.\nBez przikłod jak mosz kōnto na https://mastodon.social\n to ino napisz mastodon.social\nMożesz zaczōńć pisać piyrsze litery i pokożōm sie podpowiedzi nazw. + + Wiyncyj informacyji + + Jynzyki + Ino media + Pokoż 18+ + Przekłady Crowdin + Mynedżer Crowdin + Przekłodanie aplikacyje + Ô Crowdin + Bot + Instancyjo Pixelfed + Instancyjo Mastodona + Lecykery + Wszyjske + Żodne + Lecy kere słowa (ôddzielōne spacyjami) + Wszyjske te słowa (ôddzielōne spacyjōm) + Przidej pora słōw do filtra (ôddzielōne spacyjōm) + Przemianuj kolumna + Instancyjo Misskey + Na maszinie niy ma aplikacyje, co spiyro tyn link. + Subskrypcyje + Przeglōnd + Popularne + Niydowno przidane + Lokalne + Wgrej + Ôdpowiydz + Wymaż kōmyntorz + Na zicher wymazać tyn kōmyntorz? + Film we połnym ekranie + Tryby wideo + Wybier zbiōr do wgranio + Moje filmy + Tytuł + Licyncyjo + Kategoryjo + Jynzyk + We tym filmie sōm materyje do majoryntnych + Przizwōl na kōmyntorze do filmu + Aktualizuj film + Ôpis + Tyn film bōł zaktualizowany! + Wgrowanie pociepniynte! + Film bōł wgrany! + Wgrowanie, czekej… + Tyknij sam, żeby edytować dane filmu. + Skasuj film + Na zicher chcesz skasować tyn film? + Pokazuj filmy 18+ + Żodnych filmōw do pokozanio! + Ôstow kōmyntorz + Udostympnij + Wybier tryb udostympniynio + Ze masziny + Ze serwera + Tuty (serwer) + Tuty (maszina) + Zmiyń + Pokazuj nowe tuty nad kneflym „Ściōng wiyncyj” + Raje + Interfejs + Kōntakty + %1$s kōmyntuje twōj film %2$s]]> + %1$s śledzi twōj kanał %2$s]]> + %1$s śledzi twoje kōnto]]> + %1$s bōł ôpublikowany]]> + %1$s]]> + %1$s]]> + %1$s publikuje nowy film: %2$s]]> + %1$s bōł dodany do czornyj listy]]> + %1$s bōł wymazany ze czornyj listy]]> + Eksportuj dane + Importuj dane + Wybier zbiōr do importu + Doszło do błyndu przi edytowaniu zbioru ibrycznego! + Przidej publiczny kōmyntorz + Wyślij kōmyntorz + Niy ma połōnczynio ze internetym. Wiadōmość była spamiyntanio we kōnceptach. + Zwykły tekst + HTML + Markdown + Ôdloguj kōnto + Wszyjsko + Spiyrej aplikacyjo + Open Collective pōmogo grupōm wartko zakłodać spōłdzielnie, zbiyrać piniōndze i sprawować je przejzdrziście. + Skopiuj link + Połōncz + Normalny + Kōmpaktowy + Kōnsola + Wybier tryb pokazowanio + Spraw Dostawcy Bezpieczyństwa + Aktualizuj dōmyny śledzynio + Baza danych śledzynio była zaktualizowano! + wywołania http zablokowane ôd aplikacyje + Lista zablokowanych wywołań + Wyślij + Baza danych była wyeksportowano! + Wyrōżniōne hasztagi + Filtruj raja tagami + Żodnych tagōw + Skryj knefel „wymaż” we karcie powiadōmiyń + Przidej ôbrozek przi udostympnianiu URL + + Anketa + Ankety + Stwōrz anketa + Wybōr 1 + Wybōr 2 + Wybōr %d + Potrza aby dwōch wyborōw do ankety! + Gotowe + kōniec ô %s + Ôdświyż anketa + Weluj + Skōńczyła sie anketa ze twojim głosym + Skōńczyła sie anketa co była rozgłoszano ôd ciebie + Persōnalizuj + Kategoryje + Przedzioł czasowy + Rozszyrzōne + Pokoż znaczek „nowy” przi niyprzeczytanych tutach + Peertube + Pōnknij raja + Skryj raja + Porzōndkuj raje + Lista doimyntnie wymazano + Śledzōno instancyjo wymazano + Przipniynty tag wymazany + Cofnij + Musisz mieć dwie widzialne karty! + Porzōndkuj raje + Głōwne raje mogōm być ino skryte! + BBCode + Dycki ôznaczej media za uraźliwe + Instancyjo GNU + Status ze pamiyńci cache + Przekazuj tagi we ôdpowiedziach + Naciś dugo, żeby spamiyntać medium + Rozmaż uraźliwe media + Pokazuj raje we liście + Pokazuj raje + Ôznaczej we tutach kōnta botōw + Sprawuj tagi + Pamiyntej pozycyjo na dōmowyj raji + Historyjo + Playlisty + Miano pokazowane + Niy mosz żodnych playlist. Tyknij ikōna „+”, żeby przidać nowo + Musisz wkludzić pokazowane miano! + Kanał je potrzebny, jak playlista je publiczno. + Stwōrz playlista + Jeszcze nic niy ma na playliście. + jeszcze roz + Galeryjo + Emoji + Nalepka + Gumka + Tekst + Filter + Pyndzel + Na zicher chcesz zawrzić bez spamiyntowanio ôbrozka? + Pociep + Spamiyntowanie… + Ôbrozek spamiyntany! + Niy szło spamiyntać ôbrozka + Przejzdrzistość + Włōncz edytōr fotografiji + Przidej elymynt ankety + Wymaż ôstatni elymynt ankety + Wycisz godka + Cofnij wyciszynie godki + Godka już niy je wyciszōno! + Godka je wyciszōno + Ôtwōrz funkcyje aplikacyje + Czasowe wyciszynie + Spōmnij kōnto + Ôdświyż cache + Spōmnij status + Nowiny + Ôgōlne + Regiōnalne + Kōnszt + Dziynnikarstwo + Aktywizm + Gry + Technologijo + Treści do majoryntnych + Furry + Jodło + Logo ôd instancyje + Coś poszło źle przi wybadowaniu dostympnych instancyji! + Przistōmp do Mastodona + Wybier instancyjo bez wybranie kategoryje i tyknij knefla zaznaczanio. + Wybier instancyjo bez tykniyńcie knefla zaznaczanio. + %1$s używoczōw + Potwiyrdź hasło + Zgodzōm sie na %1$s i %2$s + prawidła serwera + warōnki usugi + Zaregistruj sie + Ta instancyjo funguje ze zaproszyniami. Twoje kōnto bydzie musiało być ryncznie potwiyrdzōne ôd administratora, podwiela bydzie szło go używać. + Wypołnij wszyjske pola! + Hasła niy pasujōm! + Email niy wyglōndo na noleżny! + Twoje miano używocza bydzie unikalne na %1$s + Dostaniesz email ze potwiyrdzyniym + Użyj aby 8 znakōw + Hasło powinno mieć aby 8 znakōw + Miano używocza powinno mieć ino litery, numery i podkryślynia + Kōnto stworzōne! + Twoje kōnto było stworzōne!\n\n + Pōmyśl nad potwiyrdzyniym swojigo emaila bez nojbliższe 48 godzin.\n\n + Możesz teroz podłōnczyć swoje kōnto bez wpisanie %1$s we piyrszym polu i tykniyńcie Połōncz.\n\n + Ważne: Jeźli twoja instancyjo wymogo weryfikacyje, to dostaniesz email, jak ôna sie udo! + + Spamiyntać wiadōmość we kōnceptach? + Administracyjo + Zgłoszynia + Żodnych zgłoszyń do pokozanio! + Podłōncz zaś kōnto + Aplikacyjo niy mogła ôtworzić funkcyji administratorskich. Możno bydzie trzeba podłōnczyć kōnto na nowo, żeby mieć połny dostymp. + Niyrozwiōnzane + Zdalne + Aktywne + Czekajōm + Zastawiōne + Wyciszōne + Zawieszōne + Uprawniynia + Status email + Status sesyje + Registracyjo + Ôstatnie IP + Ôstrzyż + Zastow + Wycisz + Dej znać używoczowi bez email + Włosne ôstrzyżynie + Używocz + Moderatōr + Administratōr + Potwiyrdzōne + Niypotwiyrdzōne + Zgłoszōne statusy + Kōnto + Cofnij cisza + Prziwrōć + Zawieś + Ôdwieś + Kōnto je wyciszōne! + Kōnto już niy je wyciszōne! + Kōnto je zawieszōne! + Kōnto już niy je zawieszōne! + Kōnto je zastawiōne! + Kōnto już niy je zastawiōne! + Kōnto było ôstrzyżōne! + Pokoż myni administracyjne + Pokazuj funkcyje administracyjne we statusach + Przizwōl + Kōnto je potwiyrdzōne! + Kōnto je ôdciepniynte! + Przidziel mie + Niy przidzielej + Ôznacz za rozwiōnzane + Ôznacz za niyrozwiōnzane + Prōzno treść! + Pokazuj knefel funkcyji Fedilab + Aplikacyjo potrzebuje dostympu do nagrowanio audio + Głosowo wiadōmość + Włōncz wartko ôdpowiydź + Kōnto, co mu ôdpowiadosz, może niy widzieć tyj wiadōmości! + Jeźli zastawiōne, aplikacyjo bydzie dycki ladować ôstatnie statusy + Jeźli zastawiōne, uraźliwe media bydōm skryte za kneflym + Spamiyntej media we połnyj srogości po dugim przitrzimaniu na podglōńdzie + Przidej knefel wielokropka na wiyrchu z prawyj ze listōm wszyjskich tagōw/instancyji/list + W czasie przedziału czasu aplikacyjo bydzie wysyłać powiadōmiynia. Możesz ôdwrōcić (bp. wyciszyć) tyn przedzioł we prawym rozwijanym myni. + Pokazuj knefel Fedilab pod ôbrozkym profilowym. To je skrōt do funkcyji aplikacyje. + Przizwōl na ôdpowiadanie bezpostrzednio we rajach pod statusami + Podglōndy niy bydōm przitniynte we rajach + Przizwolo grać wrażōne filmy bezpostrzednio we rajach + Przizwolo ôdwrōcić spusōb czytanio statusōw, co sōm pokazowane po tykniyńciu knefla „Ściōng wiyncyj” + Ta ôpcyjo przizwolo na sparcie nojnowszych zestawōw szyfrōw. Używo sie jij na starszych maszinach z Androidym, jak niy idzie sie połōnczyć ze swojōm instancyjōm. + Ino do filmōw Peertube. Przeszaltruj tyn tryb, jak niy możesz ich grać. + Te tagi przizwolōm na filtrowanie statusōw ze profili. Bydzie trzeba użyć myni kōntekstu, żeby je ôbejzdrzeć. + Autōmatycznie wraź nowo linijo po spōmniyniu, żeby zaczōńć ôd srogij litery + Przizwōl kreatorōm treści udostympniać swoje statusy we kanałach RSS + Pisanie + Limit prōb przi wgrowaniu mediōw + Stwōrz sam nowy folder + Wkludź miano foldra + Wkludź noleżne miano foldra + Taki folder już istniyje.\n Wkludź inksze miano dlo foldra + Wybier + Wychodny katalog + Folder + Stwōrz folder + Pokoż wiadōmość sukcesu po skōńczyniu akcyje (rozgłos, lubiynie itd.)? + Wyciszōne instancyje były wyeksportowane! + Przidej instancyjo + Eksportuj instancyje + Importuj instancyje + Reporty ô awaryjach + Włōncz reporty ô awaryjach + Jeźli włōnczōne, report ô awaryji bydzie stworzōny lokalnie i bydzie szło go wysłać. + Fedilab przestoł fungować :( + Idzie wysłać report ô awaryji bez email. To pōmoże go sprawić :)\n\nIdzie przidać ekstra treść. Dziynkujymy! + Użyj WYSIWYG + Jeźli włōnczōne, to bydzie szło prosto noczyniym formatować tekst. + Statystyki + Wszyjskich statusōw + Liczba rozgłosōw + Liczba polubiyń + Liczba spōmniyń + Liczba śledzyń + Liczba anket + Liczba ôdpowiedzi + Liczba statusōw + Statusy + Widzialność + Liczba ze mediami + Liczba ze uraźliwymi mediami + Liczba ze 18+ + Data piyrszego statusu + Data ôstatnigo statusu + Data piyrszego powiadōmiynio + Data ôstatnigo powiadōmiynio + Czynstość + %s statusōw na dziyń + %s powiadōmiyń na dziyń + Przedzioł dat + Grupy + Żodnych grup! + Zastow włosne animowane emoji + Diagramy + Pokoż diagramy + Aplikacyjo zbiyro twoje lokalne dane, czekej… + Kopije ibryczne + Autōmatyczne kopije ibryczne statusōw + To je ôpcyjo do kożdego kōnta. Ôna aktywuje usuga, co autōmatycznie bydzie trzimać twoje statusy lokalnie we bazie danych. To przizwoli na statystyki i diagramy + Autōmatyczne kopije ibryczne powiadōmiyń + To je ôpcyjo do kożdego kōnta. Ôna aktywuje usuga, co autōmatycznie bydzie trzimać twoje powiadōmiynia lokalnie we bazie danych. To przizwoli na statystyki i diagramy + Zgłoś kōnto + Wyślij zaproszynie + Twoja instancyjo niy przizwolo na registrowanie nowych kōnt! + + %d głos + %d głosy + %d głosōw + %d głosu + + + %d anketowany + %d anketowanych + %d anketowanych + %d anketowanego + + + Pojedynczy wybōr + Wiyncyj wyborōw + + + 5 minut + 30 minut + 1 godzina + 6 godzin + 1 dziyń + 3 dni + 7 dni + + + Przeglōndarka + Bezpostrzedni streaming + + Żeby przistōmpić do mojij instancyje „%1$s”, możesz ściōngnōńć Fedilab:\n\nF-Droid: %2$s\nGoogle: %3$s\n\nÔtwōrz link niżyj we Fedilabie i stwōrz swoje kōnto :)\n\n%4$s + + Twoja anketa niy może mieć tuplowanych ôpcyji! + Do wszyjskich kōnt + Cache bazy danych + Ôprōznij cache swojij dōmowyj raje + Wymaż swoje statusy z cache + Wymaż swoje zokłodki + Zbiory we cache + Wszyjskich powiadōmiyń + Skryj elymynta myni + Fedilab aktywuje powiadōmiynia na żywo + Do %1$s kōnt ze %2$s wydarzyniami + Powiadōmiynia do %1$s + Powiadōmiynia na żywo bydōm aktywne do tego kōnta. + Wyprōzniej cache przi zawarciu + Cache (media, spamiyntane wiadōmości, dane ze wbudowanyj przeglōndarki) bydōm autōmatycznie wymazowane przi zawiyraniu aplikacyje. + Chcesz przestać śledzić to kōnto? + Pokoż prośba ô potwierdzynie przed przestaniym śledzynio + Zamiyń Youtube na Invidio.us + Invidious to alternatywa do przodnich funkcyji YouTube + Wkludź swōj włosny host abo ôstow prōzne, żeby używać invidio.us + Zamiyń Twitter na Nitter + Nitter to ôtwartozdrzōdłowo, skupiōno na prywatności alternatywa do przodnich funkcyji Twittera. + Wkludź swōj włosny host abo ôstow prōzne, żeby używać nitter.net + Zamiyń Instagram na Bibliogram + Bibliogram to ôtwartozdrzōdłowo, skupiōno na prywatności alternatywa do przodnich funkcyji Instagrama. + Wkludź swōj włosny host abo ôstow prōzne, żeby używać bibliogram.art + Replace Reddit with Libreddit + Libreddit is an open source alternative Reddit front-end focused on privacy. + Enter your custom host or leave blank for using libredd.it + Replace Medium links + Replace medium.com links with an open source alternative front-end focused on privacy. + Default: scribe.rip + Replace Wikipedia links + Replace Wikipedia link with an open source alternative front-end focused on privacy. + Default: wikiless.org + Skryj posek powiadōmiyń Fedilab + Żeby skryć permanyntne powiadōmiynie we posku statusu, tyknij knefel z ôkym i ôdznacz „Pokoż we posku statusu” + Use a push notifications system for getting notifications in real time. + Żodnych powiadōmiyń na żywo + Live notifications + Powiadōmiynia bydōm ściōngane co 15 minut. + Przidej zopiski + Zopiski do kōnta + Przizwōl na kōmpresowanie srogich fotografiji do myńszych ze minimalnōm stratōm jakości ôbrazu. + Przizwōl na kōmpresowanie filmōw przi trzimaniu jejich jakości. + Aplikacyjo kōmpresuje media, to może trocha weznōńć… + Zmiyń ikōna aplikacyje + Tyknij, żeby zmiynić ikōna aplikacyje + Publikuj + Widzialność postu + Tyknij sam, żeby przidać fotografije + Akceptowane formaty: jpeg, png, gif \n\nMaksymalno srogość zbioru: 15 MB \n\nWe albumach może być do 4 fotografiji abo filmōw + Przidej media + Przidej ekstra podpis + Aplikacyjo dostała moc dugo wiadōmość ô felerze ze API %1$s + Podglōnd wiadōmości + Przidej spōmniynia we wiadōmości + Ściōnganie godki + Porzōndkuj podle + Tytuł filmu + Przistōmp do Peertube + Je mi aby 16 lot i zgodzōm sie na %1$s na tyj instancyji + Linki + Zmiyń farba linkōw (URL, spōmniynia, tagi itd.) we wiadōmościach + Tytuł reblogōw + Zmiyń farba pokazowanego miana na wiyrchu wiadōmości + Zmiyń farba miana używocza na wiyrchu wiadōmości + Zmiyń farba tytułu reblogōw + Posty + Zadnio farba postōw na rajach + Wysnoż farby + Tyknij sam, żeby wysnożyć wszyjske twoje włosne farby + Wysnoż + Ikōny + Farba spodnich ikōn we rajach + Przipnij tyn tag + Logo instancyje + Edytuj profil + Zrōb coś + Przekłod + Podglōnd ôbrozka + Farba tekstu + Zmiyń farba we postach + Wkludź zmiany + Musisz zresztartować aplikacyjo, żeby wkludzić zmiany + Zresztartuj + Użyj włosnego tymatu + Przizwōl na nadpisanie farbōw ôbranego tymatu wyżyj + Tymaty + Nojprzōd spamiyntej + Tymat bōł wyeksportowany + Tymat bōł wyeksportowany do CSV + Wkludź głōwno farba do poska statusu + Farba poska statusu + Prziwrōć wychodny tymat + Importuj tymat + Tyknij sam, żeby importować tymat ze poprzednigo eksportu + Eksportuj tymat + Tyknij sam, żeby eksportować teroźny tymat + Doszło do błyndu przi wybiyraniu zbioru tymatu + Wybiyrocz tymatōw + Wybier wcześnij zainstalowany tymat + Tymaty + Wkludź głōwno farba do poska nawigacyje + Farba poska nawigacyje + Farba na zadku zawartości aplikacyje. + Farba zadku + Akcyntuj wybrane party interfejsu. + Farba akcyntu + Nojczyńścij pokazowane we aplikacyji. + Głōwno farba + Eksportuj zokłodki do instancyje + Importuj zokłodki ze instancyje + Liczba używoczōw + Liczba statusōw + Liczba instancyji + Zablokowani + Kōniec za %s + Co nowego we %s + Możesz śledzić nasze kōnto, żeby dostować informacyje + Ta instancyjo niy je dostympno na https://instances.social + Pokoż cołki link + Udostympnij link + Adresa URL była skopiowano do skrytki + Ôtwōrz we inkszyj aplikacyji + Wybadej przekerowanie + Ta adresa URL niy przekerowuje + %1$s \n\nprzekerowuje do\n\n %2$s + Zmiyń agynta używocza + Nasztaluj włosnego agynta używocza abo ôstow prōzne + Przizwolo na edytowanie agynta używocza do wywołań API abo we wbudowanyj przeglōndarce. + Wymaż parametry UTM + Aplikacyjo autōmatycznie wymaże parametry UTM ze adresy URL przed ôtwarciym linku. + Tryndy + Teroz we tryńdzie + %d ludzi godo + Kōnta Twitter (bez Nitter) + Miana używoczōw Twittera ôddzielōne spacyjami + Dowody tożsamości + Zweryfikowano tożsamość + Zweryfikowane ôd %1$s (%2$s) + Wymaż powiadōmiynie + Pokoż wiyncyj ôpcyji + To je historyjo ze Pixelfed + Wgrej medium, ôno bydzie autōmatycznie przidane do twojij historyje Pixelfed. + Medium przidane do twojij historyje! + Akcyjo niyaktywno + Niy śledź + Coś poszło źle, wejzdrzij do swojigo katalogu pobiyranio we sztelōnkach. + Ôgłoszynia + Żodnych ôgłoszyń! + Przidej reakcyjo + Użyj swojij preferowanyj przeglōndarki we aplikacyji. Ôdznacz ta funkcyjo, żeby ôtwiyrać linki zewnyntrznie. + Cache wideo we MB, zero znaczy brak cache. + Znaki wodne + Dodej autōmatycznie znak wodny na spodku ôbrozkōw. Inkszy tekst może być nasztalowany do kożdego kōnta. + No distributors found! + You need a distributor for receiving push notifications.\nYou will find more details at %1$s.\n\nYou can also disable push notifications in settings for ignoring that message. + Select a distributor + diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml new file mode 100644 index 000000000..f96d26c1f --- /dev/null +++ b/app/src/main/res/values-tr/strings.xml @@ -0,0 +1,1136 @@ + + + Menüyü aç + Menüyü kapat + Hakkında + Sunucu hakkında + Gizlilik + Önbellek + Çıkış + Giriş + + Kapat + Evet + Hayır + Iptal + İndir + İndirme: %1$s + Medya kaydedildi + Dosya: %1$s + Şifre + E-posta + Hesaplar + Tootlar + Etiketler + Kaydet + Geri yükle + Sonuç bulunamadı! + Sunucu + Sunucu: mastodon.social + Şimdi %1$s hesabın aktif + Hesap ekle + Toot içeriği panoya kopyalanmış + Gönderinin URL\'si panoya kopyalandı + Değiştir + Resim seç… + Temizle + Kamera + Tümünü sil + Bu toot\'u çevir. + Zamanla + Metin ve simge boyutları + Geçerli metin boyutunu değiştirin: + Geçerli simge boyutunu değiştir: + İleri + Önceki + Birlikte aç + Onayla + Medya + Şununla paylaş + Fedilab ile paylaşıldı + Cevaplar + Kullanıcı adı + Taslaklar + Favoriler + Yeni Takipçiler + Söz edilenler + Arttırılanlar + Arttırılanları göster + Yanıtları göster + Tarayıcıda aç + Çevir + Lütfen, bu işlemi yapmadan önce birkaç saniye bekleyin. + + Ana Sayfa + Yerel zaman çizelgesi + Federasyon zaman çizelgesi + Ayarlar + Favoriler + İletişim + Susturulmuş Kullanıcılar + Engellenen Kullanıcılar + Bildirimler + Takip İsteği + Ayarlar + Hesabı sil + %1$s hesabı uygulamadan silinsin mi? + Bana bir e-posta gönder + Değiştirmek için yola tıklayın + Başarısız! + Zamanlanmış tootlar + Aşağıdaki bilgiler kullanıcının profilini eksik gösterebilir. + İfade ekle + Uygulama şu an için özel ifade toplayamadı. + Anlık bildirimler + Oturumu kapatmak istediğinizden emin misiniz? + \@%1$s@%2$s oturumunu kapatmak istediğinizden emin misiniz? + + Görüntülenecek yazı yok + Gösterilecek hikaye yok + Hikayeler + %1$s arttırdı + Bu toot\'u favorilere ekleyin? + Bu toot\'u favorilerden kaldırın? + Bu toot\'u arttır? + Bu toot\'u arttırma? + Bu toot\'u sabitle? + Bu toot\'u sabitlemekten vazgeç? + Sustur + Engelle + Rapor Et + Kaldır + Kopyala + Paylaş + Bahset + Zamanlanmış susturma + Sil & yeniden ekle + + Bu hesabı sustur? + Bu hesabı engelle? + Bu toot\'u bildir? + Bu alan adı engellensin mi? + Bu hesabın susturulması açılsın mı? + Hesabın engeli kaldırılsın mı? + + + Bilgilendir + Sessiz + + + Bu toot\'u sil? + Gönderi taslaklara kaydedilip & silinsin mi? + + Yer İmleri + Yer imlerine ekle + Yer imini kaldır + Gösterilecek yer imi yok + Durum yer imlerine eklendi! + Durum yer imlerinden kaldırıldı! + + %d s + %d m + %d h + %d d + + %d saniye + %d saniye + + + %d dakika + %d dakika + + + %d saat + %d saat + + + %d gün + %d gün + + + Uyarı + Aklınızda ki nedir? + TOOT! + QUEET! + cv + Toot yaz + Toot\'a yanıt ver + Bir queet yaz + Queet\'e yanıt ver + Ortam dosyası seçin + Medya dosyasını seçerken bir hata oluştu! + Bu ortamı sil? + Toot boş! + Toot görünürlüğü + Toot\'ların varsayılan görünürlüğü: + Toot gönderildi! + Bu toot\'u yanıtlıyorsun: + Hassas içerik? + + Herkese açık zaman tüneline gönder + Herkese açık zaman tüneline gönderme + Takipçilere gönder + Sadece bahsedilen kullanıcılara gönder + + Taslak Yok! + Bir toot seçiniz + Bir hesap seçin + Birden fazla hesap seçin + Taslak kaldırılsın mı? + Orijinal toot\'u görüntülemek için düğmeye dokun + Görme engelliler için tarif + + Açıklama bulunmuyor! + + Sürüm %1$s + Geliştirici: + Lisans: + GNU GPL V3 + Kaynak kodu: + Toot\'un tercümesi: + Sunucuları ara: + İkon tararımcısı: + + Görüşme + + Gösterilecek hesap yok + Takip İsteği yok + Gönderiler \n %1$s + Takip Edilen \n %1$s + Takip Eden \n %1$s + Sabitlenmiş \n %d + İzin Ver + Reddet + + Gösterilecek zamanlanmış toot yok! + Bir modül yazmak ve üst menüden zamanlamasını seçin. + Zamanlanmış toot silinsin mi? + Medya: %d + Toot zamanlandı! + Planlanan tarih geçerli saatten daha büyük olması gerekir! + Enerji tasarrufu aktif! Beklendiği gibi çalışmayabilir. + + Susturulacak süre, bir dakikadan fazla olmalı. + %2$s tarihine kadar %1$s susturuldu.\n Susturmaktan vazgeçmek için kullanıcının profiline git. + %2$s tarihine kadar %1$s susturuldu.\n Vazgeçmek için dokun. + + Görüntülenecek bildirim yok + senden bahsetti + yeni bir mesaj yazdı + durumunu arttırdı + durumunu favorilere ekledi + seni takip etti + sizi takip etmek istiyor + + ve başka bildirim + ve başka bildirim + + + %d beğenme + %d beğeni + + Bildirimi sil? + Bildirimleri sil? + Bildirim silindi! + Bildirimler silindi! + + Takip Ediliyor + Takipçiler + Sabitlenmiş + + Müşteri kimliği alınamıyor! + Sunucu alan adına bağlanılamıyor! + İnternet bağlantısı yok! + Hesap engellenmiş! + Bu hesap artık engelli değil! + Hesap susturuldu! + Bu hesap artık susturulmuş değil! + Hesap takip edildi! + Bu hesap artık takip edilmiyor! + Toot arttırıldı! + Toot artık arttırılmış değil! + Toot favorilere eklendi! + Toot favorilerden kaldırıldı! + Toot rapor edildi! + Toot silindi! + Toot sabitlendi! + Toot artık sabitlenmiyor! + Üzgünüz ! Bir hata oluştu! + Bir hata oluştu! Sunucu bir yetki kodu dönmedi! + Sunucu alan adı geçerli değil gibi görünüyor! + Hesaplar arasında geçiş yapılırken bir hata oluştu! + Arama sırasında bir hata oluştu! + Profil verileri kaydedildi! + Yapılabilecek bir işlem yok + Medya kaydedildi! + Arama sırasında bir hata oluştu! + Çeviriler ayarlardan devre dışı bırakıldı + Taslak kaydedildi! + Sunucunun bu kadar karaktere izin verdiğinden emin misin? Genelde, 500 civarı karakter olabilir. + Toot\'ların görünürlüğünü hesap %1$s için değiştirildi + + Yükleme başına toot + Her zaman + WIFI + Sor + Medya yükle + Resimleri yükle + Daha fazla göster… + Daha az göster… + Hassas içerik + Haraketli Avatarları Devre Dışı Bırak + Yolu: + Taslakları otomati̇k olarak kaydet + Toot\'taki medyanın URL\'sini ekle + Biri sizi takip ettiğinde bildir + Birisi durumunuzu artırdığında bildir + Biri durumunuzu favorilere eklerse bildir + Biri kullanıcıyı takip ettiğinde bildir + Bir anket sona erdiğinde bildir + Yeni iletiler için bildirim gönder + Toot arttırmadan önce onay kutusu göster + Favorilere eklemeden önce onay kutusu göster + Sadece Wi-Fi bağlı iken bildirimleri aç + Bildir? + Sessiz bildirimler + NSFW görünümü zaman aşımı (saniye, kapatmak için sıfır girin) + Medya Açıklaması zaman aşımı (saniye, 0 kapalı demektir) + Profil düzenle + Özel paylaşım + Özel paylaşım URL… + Özgeçmiş… + Hesabı Kilitle + Değişiklikleri kaydet + Başlık fotoğrafı seçin + Küçük resimler sığdırılsın + 500 karakterden fazla cevapları otomatik olarak parçala + 160 karakter sınırına ulaştınız! + 30 karakter sınırına ulaştınız! + Arasında + ila + Saat %1$s\'den geç olmalıdır + Saat %1$s\'den erken olmalıdır + Başlama zamanı + Bitiş zamanı + Gömülü tarayıcıyı kullan + Özel sekmeler + JavaScript\'i etkinleştir + cw\'yi otomatik aç + Üçünçü parti çerezlere izin ver + API anahtarınızı Yandex için boş bırakabilirsiniz + + Koyu + Açık + Siyah + + LED rengini ayarla: + + Mavi + Deniz Mavisi + Eflatun + Yeşil + Kırmızı + Sarı + Beyaz + + Takip Et + Engeli kaldır + Sustur + Sesi aç + İstek gönderildi + Sizi takip ediyor + Ara + Cevaplarda ilk harf büyük olsun + Resimleri yeniden boyutlandır + Videoları yeniden boyutlandır + + Anlık Bildirimler + Lütfen, almak istediğiniz bildirimleri onaylayın. +         Bu bildirimleri daha sonra ayarlarda etkinleştirebilir veya devre dışı bırakabilirsiniz (Bildirimler sekmesi). + + Önbelleği temizle + Önbellekte bazı öğeler %1$s var.\n\n Onlar da silinsin mi? + Mb + Önbelleği temizlendi! %1$s serbest bırakıldı + + Başlık + Başlık… + Açıklama + Anahtar Sözcükler + Anahtar Sözcükler… + + Eşitle + Filtrele + Senin toot\'ların + Bildirimlerin + Herkese Açık + Listelenmiyor + Özel + Direk + Birkaç anahtar kelime… + Medyayı göster + Sabitlenmişleri göster + Sonuç bulunamadı! + %1$s için toot\'ları yedekle + %1$s yeni toot alındı + %1$s yeni bildirim alındı + + Tarih azalan + Tarih artan + + + Hayır + Sadece + Her ikisi de + + Veritabanında toot bulunamadı. Menüdeki eşitle tuşunu kullan. + + Kaydedilen veri + Hesaplardan yalnızca temel bilgiler cihazda saklanır. +         Bu veriler kesinlikle gizlidir ve yalnızca uygulama tarafından kullanılabilir. +         Uygulamanın silinmesi, bu verileri derhal kaldırır. \ N +         ⚠ Oturum açma ve parolalar asla saklanmaz. Yalnızca sunucu ile güvenli giriş (SSL) için kullanılırlar. + Izinler: + - ACCESS_NETWORK_STATE : Aygıtın bir WIFI ağına bağlı olup olmadığını saptamak için kullanılır. \n +         - INTERNET : Bir sunucuyu sorgulamak için kullanılır. \n +         - WRITE_EXTERNAL_STORAGE : Medyayı saklamak veya uygulamayı bir SD karta taşımak için kullanılır. \n +         - READ_EXTERNAL_STORAGE : Toot\'a medya eklemek için kullanılır.\n +         - BOOT_COMPLETED : Bildirim hizmetini başlatmak için kullanılır. \n +         - WAKE_LOCK : Bildirim hizmeti sırasında kullanılır. + API izinleri: + - Oku : Veri okunur. \n +         - Yaz : Durumu yayınlar ve durumlar için ortam yükler. \n +         - Takip et : Takip etme, takipten vazgeçme, engelleme, engeli kaldırma.\n\n +         ⚠ Bu eylemler, yalnızca kullanıcı istekleri üzerine gerçekleşir. + İzleme ve kütüphaneler + Bu uygulama takip araçları kullanmaz (kitle ölçme, hata raporlama vs.) ve herhangi bir reklam içermez.\n\n + Kütüphane kullanımı en aza indirilmiştir: \n + - Glide: Medya kontrolü\n + - Android-Job: Servis kontrolü\n + - PhotoView: Resim kontrolü\n + + Toot tercümesi + Uygulama cihazın yerel ayarını ve Yandex API\'sini kullanarak toot tercüme olanağı sunar. \ N +         Yandex\'in şu adreste bulunabilecek uygun gizlilik politikası vardır: https://yandex.ru/legal/confidential/?lang=en + Teşekkürler: + Düzenli ifadelerle filtreleme + Ara + Sil + Daha çok toot yükle… + + Listeler + Bu listeyi tamamen silmek istediğinden emin misin? + Bu listede henüz hiçbir şey yok. Bu listenin üyelerinin yeni gönderileri burada gözükecek. + Listeye ekle + Liste ekle + Listeyi sil + Listeyi düzenle + Yeni liste başlığı + Hesap listeye eklendi! + Henüz bir listeniz yok! + + %1$s, %2$s\'e taşındı + Kimlik doğrulama çalışmıyor? + Yardımı olabilecek bazı kontroller:\n\n + - Sunucu adını kontrol et\n\n + - Sunucunda hata olmadığından emin ol\n\n + - İki aşamalı doğrulama (2FA) kullanıyorsan, alttaki bağlantıyı kullan (sunucu adı doldurulduktan sonra)\n\n + - Bağlantıyı 2FA olmadan da kullanabilirsin\n\n + - Eğer hala çalışmıyorsa, Framagit üzerinde hata kaydı aç: https://framagit.org/tom79/fedilab/issues + + Medya yüklendi. Görüntülemek için tıkla. + Bu işlem oldukça uzun sürebilir. Bitince haberdar olacaksın. + Hâlâ çalışıyor, lütfen bekle… + Durumları dışarı aktar + %1$s için durumları dışarı aktar + %2$s toot\'tan %1$s tanesi dışarı aktarıldı. + %1$s toot\'ları dışarı aktarılırken sorun çıktı + Veri dışa aktarılırken bir şeyler ters gitti! + Veri içe aktarılırken bir şeyler ters gitti! + + Proxy + Proxy aç? + Sunucu + Port + Giriş + Parola + Paylaşırken toot detayları ekle + Uygulamayı Liberapay ile destekle + Düzenli ifadede sorun var! + Bu sunucuda hiç zaman tüneli bulunamadı! + Bu sunucuyu sil? + Şu dile çevir: + Sunucuyu takip et + Bu sunucuyu zaten takip ediyorsun! + Durum takip edildi! + İş birliği + Bilgi + Yinelemelerini gizle %s + Profilde göster + Yinelemelerini göster %s + Profilde gösterme + Hesap şimdi profilde gösteriliyor + Hesap artık profilde gösterilmiyor + Yinelemeler şimdi gösterildi! + Yinelemeler şimdi gizlendi! + Doğrudan mesaj + Filtreler + Görüntülenecek filtre yok. \"+\" Düğmesine tıklayarak bir tane oluşturabilirsiniz. + Anahtar kelime veya ifade + Anasayfa + Genel zaman çizelgesi + Bildirimler + Görüşmeler + Metnin büyük/küçük harf durumundan veya gönderinin içerik uyarısından bağımsız olarak eşleştirilecek + Gizlemek yerine bırak + Filtre uygulanmış gönderiler, filtre daha sonra kaldırılsa bile geri döndürülemez biçimde kaybolacaktır + Anahtar kelime veya ifade sadece harfler ve rakamlardan oluştuğunda, yalnızca tam sözcükle eşleşirse uygulanacaktır + Tam sözcük + Filtre içerikleri + Filtrenin uygulanacağı bir veya daha fazla içerik + Bitiş tarihi + Filtreyi sil? + Filtreyi güncelle + Filtre oluştur + Takip edilecekler + Şu an için listelenen herhangi bir hesap yok! + Takip et + Tümünü seç + Tümünün seçimini kaldır + %s takip ediliyor! + %s listesi oluşturuluyor + Hesaplar listeye ekleniyor + Hesaplar listeye eklendi + Listeye hesaplar ekleniyor + Henüz bir liste oluşturmadınız. Yeni bir tane eklemek için \"+\" butonuna basın. + Takip edebileceklerin + Trunk API + Hesap(lar) takip edilemez + Uzak hesap alınıyor + Gizli medyayı otomatik olarak genişlet + Yeni takip + Yeni Yineleme + Yeni Favori + Yeni Bahsetme + Anket Sona Erdi + Yeni Toot + Toot Yedekleme + Yeni iletiler + Medya İndirme + Bildirim sesini değiştir + Ton Seçin + Zaman dilimini etkinleştir + Nasıl Yapılır Videoları + Uzak sohbet alınıyor! + Engellenen alan adı yok! + Alan adının engelini kaldır + %s\'in engelini kaldırmaktan emin misiniz? + %s\'in engellemekten emin misiniz?\n\nBu etki alanından herhangi bir içeriği herhangi bir genel zaman çizelgesinde veya bildirimlerinizde göremezsiniz. Bu alan adındaki takipçileriniz kaldırılacak. + Engellenen alan adları + Alan adını engelle + Alan adı engellendi + Alan adı artık engelli değil! + Uzak durum alınıyor + Yorum + Peertube sunucusu + Sağ üstteki buton ile bu videoya ilk yorum yapan siz olun! + %s izleme + Süre: %s + Sunucu ekle + Bu videoda yorumlar etkin değil! + Bir çözünürlük seçin + Peertube favorileri + Video yer imlerine eklendi! + Video yer imlerinden kaldırıldı! + Favorilerinizde hiç Peertube videosu yok! + Kanal + Videolar + Kanallar + Emoji One kullan + Bilgi + Tüm tootlarda ön izlemeleri göster + Yeni UX/UI tasarımcısı + Video ön izlemelerini göster + Hesap kimliği panoya kopyalandı! + Dili değiştir + Varsayılan dil + Uzun tootları kırp + \'x\' satırdan uzun tootları kırp. Sıfır, devre dışı anlamına gelir. + Daha fazla göster + Daha az göster + Etiketleri yönet + Etiket zaten mevcut! + Etiket kaydedildi! + Etiket değiştirildi! + Etiket silindi! + Arttırma zamanla + Yineleme zamanlandı! + Gösterilecek zamanlanmış arttırma yok! + Arttırma zamanla\'yı seçin.]]> + Sanatsal zaman çizelgesi + Menüyü aç + Geri git + Uygulamanın logosu + Profil resmi + Profil afişi + Sunucunun yöneticisiyle iletişime geç + Yeni ekle + MastoHost logosu + Emoji seçici + Yenile + Görüşmeyi genişlet + Hesap kaldır + Engellenen alan adını sil + Özel emoji seçici + Videoyu oynat + Yeni toot + Kartın görüntüsü + Medyayı gizle + Favicon + Medya için açıklama ekleyin (görme engelliler için) + + Asla + 30 dakika + 1 saat + 6 saat + 12 saat + 1 gün + 1 hafta + + In this field, you need to write your instance host name.\nFor example, if you created your account on https://mastodon.social\nJust write mastodon.social (without https://)\n + You can start writing first letters and names will be suggested.\n\n + ⚠ The Login button will only work if the instance name is valid and the instance is up! + + Daha fazla bilgi + + Diller + Sadece medya + NSFW göster + Crowdin çevirileri + Crowdin yöneticisi + Uygulamanın çevirisi + Crowdin Hakkında + Bot + Pixelfed sunucusu + Mastodon sunucusu + Herhangi biri + Bunların hepsi + Bunların hiçbiri + Bu kelimelerden herhangi biri (boşlukla ayrılmış) + Bu kelimelerin tamamı (boşlukla ayrılmış) + Filtrelenecek birkaç kelime girin (boşlukla ayrılmış) + Sütun adını değiştir + Misskey sunucusu + Bu bağlantıyı destekleyen hiçbir uygulama cihazınızda yüklü değil. + Abonelikler + Genel görünüm + Trendler + Son eklenenler + Yerel + Yükle + Yanıtla + Yorumu sil + Bu yorumu silmek istediğinizden emin misiniz? + Tam ekran video + Video modu + Yüklemek için bir dosya seçin + Videolarım + Başlık + Lisans + Kategori + Dil + Bu video yetişkinler için veya sansürsüz içerik içermektedir + Video yorumlarını etkinleştir + Videoyu güncelle + Açıklama + Video güncellendi! + Karşıya yükleme iptal edildi! + Video karşıya yüklendi! + Karşıya yükleniyor, lütfen bekleyin… + Video verisini düzenlemek için buraya dokunun. + Videoyu sil + Bu videoyu silmek istediğinizden emin misiniz? + NSFW videoları göster + Gösterilecek video yok! + Yorum bırak + Paylaş + Bir zamanlama türü seçin + Cihazdan + Sunucudan + Tootlar (Sunucu) + Tootlar (Cihaz) + Değiştir + Yeni tootları \"Daha fazla yükle\" düğmesinin üzerinde göster + Zaman çizelgeleri + Arayüz + Bağlantılar + %1$s, %2$s videonuza yorum yaptı]]> + %1$s, %2$s kanalınızı takip ediyor]]> + %1$s hesabınızı takip ediyor]]> + %1$s videonuz yayınlandı]]> + %1$s video içe aktarmanız başarılı oldu]]> + %1$s video içe aktarmanız başarısız oldu]]> + %1$s yeni bir video yayınladı: %2$s]]> + %1$s videonuz kara listeye alındı]]> + %1$s videonuz kara listeden çıkarıldı]]> + Verileri dışa aktar + Verileri içe aktar + İçe aktarılacak dosyayı seçin + Yedekleme dosyasını seçerken bir hata oluştu! + Herkese açık bir yorum ekle + Yorum gönder + İnternet bağlantısı yok. Mesajınız taslaklara kaydedildi. + Düz metin + HTML + Markdown + Hesabın oturumunu kapat + Hepsi + Uygulamayı destekleyin + Open Collective, grupların hızlı bir şekilde bir birlik oluşturmalarını, para toplamalarını ve onları şeffaf bir şekilde yönetmelerini sağlar. + Bağlantıyı kopyala + Bağlan + Normal + Kompakt + Konsol + Görüntü modunu ayarla + Güvenlik Sağlayıcısı yamalarını uygula + Takip edilen alan adlarını güncelle + Takip edilenler verileri güncellendi! + http çağrıları uygulama tarafından engellendi + Engellenen çağrıların listesi + Gönder + Veritabanı dışa aktarıldı! + Öne çıkan etiketler + Zaman çizelgesini etiketlerle filtrele + Etiket yok + Bildirim sekmesinde \"sil\" butonunu gizle + URL paylaşırken bir resim ekleyin + + Anket + Anketler + Anket oluştur + 1. seçenek + 2. seçenek + %d. seçenek + Anket için en az iki seçeneğe ihtiyacınız var! + Bitti + bitiş: %s + Anketi yenile + Oy ver + Oy verdiğiniz bir anket sona erdi + Tootladığınız bir anket sona erdi + Özelleştir + Kategoriler + Zaman dilimi + Gelişmiş + Okunmamış tootlarda \'yeni\' rozetini göster + Peertube + Zaman çizelgesini taşı + Zaman çizelgesini gizle + Zaman çizelgelerini yeniden sırala + Liste kalıcı olarak silindi + Sunucu takipten çıkarıldı + Sabitlenmiş etiket kaldırıldı + Geri al + İki görünür sekme tutmanız gerekmektedir! + Zaman çizelgelerini yeniden sırala + Ana zaman çizelgeleri sadece gizlenebilir! + BBCode + Medyayı her zaman hassas olarak işaretle + GNU sunucusu + Önbelleğe alınmış durum + Yanıtlardaki etiketleri ilet + Medyayı kaydetmek için basılı tut + Hassas medyayı bulanıklaştır + Zaman çizelgelerini bir listede göster + Zaman çizelgelerini göster + Tootlardaki bot hesaplarını işaretle + Etiketleri yönet + Ana zaman çizelgesindeki konumu hatırla + Geçmiş + Çalma listeleri + Görünen isim + Hiç çalma listeniz yok. Yeni bir çalma listesi eklemek için \"+\" simgesine dokunun + Bir ekran adı sağlamanız gerekmektedir! + Çalma listesi herkese açık olduğunda kanal gereklidir. + Çalma listesi oluştur + Bu çalma listesinde henüz hiçbir şey yok. + ileri al + Galeri + Emoji + Etiket + Silgi + Metin + Filtre + Fırça + Resmi kaydetmeden çıkmak istediğinize emin misiniz? + Vazgeç + Kaydediliyor… + Resim Başarıyla Kaydedildi! + Resim kaydedilemedi + Opaklık + Fotoğraf düzenleyiciyi etkinleştir + Anket ögesi ekle + Son anket ögesini kaldır + Görüşmeyi Sustur + Görüşmeyi susturmaktan vazgeç + Görüşme artık susturulmuyor! + Görüşme susturuluyor + Uygulama özelliklerini aç + Zamanlanmış susturma + Hesaptan bahset + Önbelleği yenile + Durumdan bahset + Haberler + Genel + Yerel + Sanat + Gazetecilik + Aktivizm + Oyun + Teknoloji + Yetişkin içerik + Kürklü + Yemek + Sunucunun logosu + Mevcut sunucuları kontrol ederken bir şeyler ters gitti! + Mastodon\'a katıl + Sunucu seçmek için önce bir kategori seçin, ardından onay düğmesine dokunun. + Onay düğmesine dokunarak bir sunucu seçin. + %1$s kullanıcı + Parolayı doğrula + %1$s ve %2$s\'i kabul ediyorum + sunucu kuralları + hizmet koşulları + Kaydol + Bu sunucu davetiye gerektirmektedir. Hesabınızı kullanmadan önce yönetici onayı gerekecektir. + Lütfen tüm alanları doldurun! + Parolalar eşleşmiyor! + E-posta geçerli görünmüyor! + Kullanıcı adınız %1$s üzerinde eşsiz olacaktır + Bir onay e-postası alacaksınız + En az 8 karakter kullanın + Parola en az 8 karakter içermelidir + Kullanıcı adı sadece harf, rakam ve alt çizgi içermelidir + Hesap oluşturuldu! + Your account has been created!\n\n + Think to validate your email within the 48 next hours.\n\n + You can now connect your account by writing %1$s in the first field and click on Connect.\n\n + Important: If your instance required validation, you will receive an email once it is validated! + + Mesaj, taslaklara kaydedilsin mi? + Yönetim + Raporlar + Görüntülenecek rapor yok! + Hesabı yeniden bağla + Uygulama, yönetici özelliklerine erişemedi. Doğru kapsama sahip olması için hesabınızı yeniden bağlamanız gerekebilir. + Çözülmemiş + Uzak + Aktif + Bekleyen + Devre dışı bırakıldı + Susturuldu + Askıya alındı + İzinler + E-posta durumu + Oturum açma durumu + Katılım tarihi + En son IP + Uyar + Devre dışı bırak + Sustur + Kullanıcıyı e-posta ile bilgilendir + Özel uyarı + Kullanıcı + Moderatör + Yönetici + Doğrulandı + Doğrulanmadı + Bildirilen durumlar + Hesap + Susturmayı geri al + Devre dışı bırakmayı geri al + Askıya al + Askıya almayı geri al + Hesap susturuldu! + Hesap artık susturulmuş değil! + Hesap askıya alındı! + Hesap artık askıya alınmış değil! + Hesap devre dışı bırakıldı! + Hesap artık devre dışı değil! + Hesap uyarıldı! + Yönetici menüsünü görüntüle + Durumlarda yönetici özelliğini göster + İzin ver + Hesap onaylandı! + Hesap reddedildi! + Bana ata + Atamayı kaldır + Çözüldü olarak işaretle + Çözülmedi olarak işaretle + Boş içerik! + Fedilab özellikleri butonunu göster + Uygulamanın ses kaydetmeye erişmesi gerekiyor + Sesli mesaj + Hızlı yanıtlamayı etkinleştir + Yanıtladığınız hesap mesajınızı göremeyebilir! + Devre dışı bırakılırsa, uygulama her zaman son durumları yükleyecektir + Devre dışı bırakılırsa, hassas medya bir buton ile gizlenecektir + Önizlemelere uzun basarak medyayı tam boyutunda kaydedin + Sağ üste tüm etiketleri/sunucları/listeleri göstermek için \'üç nokta\' düğmesi ekle + Bu zaman aralığınında uygulamadan bildirim alacaksınız. Tersine çevirmek, yani bu aralıkta susturmak için sağdaki kutuyu kullanın. + Profil resminin altında bir Fedilab butonu göster. Bu, uygulama içi özelliklere erişmek için bir kısayoldur. + Doğrudan zaman çizelgelerinde, durumların altında yanıt vermeye izin ver + Önizlemeler, zaman çizelgelerinde kırpılmayacak + Gömülü videoları doğrudan zaman çizelgelerinde oynatmaya izin ver + Daha fazla getir düğmesine dokunduktan sonra görüntülenen durumları okuma şeklini tersine çevirmeye izin ver + Bu seçenek güncel şifreleme yöntemlerini desteklemeye izin verir. Eski Android cihazlar için veya sunucunuza bağlanamıyorsanız işe yarayabilir. + Sadece Peertube videoları için. Eğer onları oynatamıyorsanız bu modu değiştirin. + Bu etiketler, durumların profillerden filtrelenmesine olanak tanıyacaktır. Görmek için içerik menüsünü kullanmanız gerekecektir. + İlk harfi büyük harf yapmak için bahsetmeden sonra otomatik olarak satır sonu karakteri ekle + İçerik oluşturucuların durumlarını RSS yayınlarıyla paylaşmasına izin ver + Oluştur + Medyayı karşıya yüklerken maksimum yeniden deneme sayısı + Burada yeni bir Klasör oluştur + Klasör adını girin + Lütfen geçerli bir klasör adı girin + Bu klasör zaten var.\n Lütfen klasör için başka bir isim girin + Seç + Varsayılan Dizin + Klasör + Klasör oluştur + Bir eylem tamamlandıktan sonra (yineleme, favorileme vb.) geri bildirim mesajı görüntülemek ister misiniz? + Susturulan sunucular dışa aktarıldı! + Sunucu ekle + Sunucuları dışa aktar + Sunucuları içe aktar + Hata raporları + Hata raporlarını gönder + Etkinleştirilirse, yerel olarak bir kilitlenme raporu oluşturulacak ve ardından bunu paylaşabileceksiniz. + Fedilab durdu :( + Bana çökme raporunu e-posta ile gönderebilirsiniz. Düzeltilmesine yardımcı olacaktır :) \n\nEk içerik ekleyebilirsiniz. Teşekkür ederim! + Wysiwyg kullan + Etkinleştirildiğinde, metinleri araçlarla kolayca biçimlendirebileceksiniz. + İstatistikler + Tüm Durumlar + Yineleme sayısı + Favori sayısı + Bahsetme sayısı + Toplam takip sayısı + Anket sayısı + Yanıt sayısı + Durum sayısı + Durumlar + Görüntülenme + Medya sayısı + Hassas medya sayısı + Number with CW + İlk durum tarihi + Son durum tarihi + İlk bildirim tarihi + Son bildirim tarihi + Sıklık + Günde %s durum + günde %s bildirim + Tarih Aralığı + Gruplar + Grup yok! + Özel animasyonlu emojiyi devre dışı bırak + Grafikler + Grafikleri görüntüle + Uygulama yerel verilerinizi toplar, lütfen bekleyin... + Yedekle + Otomatik yedekleme durumu + Bu seçenek hesap başına. Durumlarınızı otomatik yerel olarak veritabanında depolayacak bir hizmet başlatır. Bu istatistik ve çizelgeleri elde etmenizi sağlar + Otomatik yedekleme bildirimleri + Bu seçenek hesap başına. Bildirimlerinizi otomatik veritabanında yerel olarak depolayacak bir hizmet başlatır. Bu istatistik ve çizelgeleri elde etmenizi sağlar + Hesabı bildir + Davetiye gönder + Sunucunuz yeni hesap açılmasına izin vermiyor! + + %d oy + %d oy + + + %d oy veren + %d oy veren + + + Tekten seçmeli + Çoktan seçmeli + + + 5 dakika + 30 dakika + 1 saat + 6 saat + 1 gün + 3 gün + 7 gün + + + Web görünümü + Doğrudan akış + + Sunucuma \"%1$s\" katılmak için Fedilab\'ı indirebilirsiniz:\n\nF-Droid:%2$s\nGoogle:%3$s\n\nArdından aşağıdaki bağlantıyı Fedilab ile açın ve hesabınızı oluşturun :)\n\n%4$s + + Ankette aynı seçenek tekrarlanmamalı! + Tüm hesaplar için + Veritabanı ön belleği + Ana zaman çizelgesi ön belleğini temizleyin + Ön belleğe alınmış durumlarınızı silin + Yer işaretlerinizi temizleyin + Önbellekteki dosyalar + Toplam bildirim + Menü öğelerini gizle + Fedilab canlı bildirimleri indiriyor + %2$s etkinliği olan %1$s hesapları için + %1$s için canlı bildirimler + Canlı bildirimler yalnızca bu hesap için etkinleştirilecek. + Çıkışta önbelleği temizle + Önbellek (medya, önbelleğe alınmış mesajlar, yerleşik tarayıcıdan gelen veriler) uygulamadan çıkarken otomatik olarak silinecek. + Bu hesabı takipi bırakmak istiyor musunuz? + Takibi bırakmadan önce onay kutusu göster + Youtube\'u Invidio.us ile değiştirin + Invidious, YouTube\'a alternatif bir arayüzdür + Özel barındırıcınızı girin veya invidious.snopyta.org\'u kullanmak için boş bırakın + Twitter\'ı Nitter ile Değiştirin + Nitter, gizliliğe odaklanan açık kaynaklı bir alternatif Twitter arayüzüdür. + Özel barındırıcınızı girin veya nitter.net\'i kullanmak için boş bırakın + Instagram\'ı Bibliogram ile Değiştirin + Bibliogram, mahremiyete odaklanan açık kaynaklı bir alternatif Instagram arayüzdür. + Özel barındırıcınızı girin veya bibliogram.art\'i kullanmak için boş bırakın + Reddit yerine Libreddit kullan + Libreddit, mahremiyete odaklanan açık kaynaklı bir alternatif Reddit arayüzüdür. + Sunucu adresi girin ya da libredd.it sunucusunu kullanmak için boş bırakın + Replace Medium links + Replace medium.com links with an open source alternative front-end focused on privacy. + Default: scribe.rip + Replace Wikipedia links + Replace Wikipedia link with an open source alternative front-end focused on privacy. + Default: wikiless.org + Fedilab bildirim çubuğunu gizle + Durum çubuğunda kalan bildirimi gizlemek için, göz simgesine dokunun ve ardından \"Durum çubuğunda göster\" seçeneğinin işaretini kaldırın + Anlık bildirimler sistemini kullanarak bildirimleri anında alabilirsiniz. + Canlı bildirim yok + Live notifications + Bildirimler 15 dakikada bir indirilecek. + Not ekle + Hesap için notlar + Görüntü kalitesinde çok az veya göz ardı edilebir kayıpla büyük fotoğrafları daha küçük boyutlu fotoğraflar halinde sıkıştırmaya izin verin. + Kalitelerini korurken videoların sıkıştırılmasına izin verin. + Uygulama medyayı sıkıştırıyor, bu biraz zaman alabilir… + Uygulamanın simgesini değiştir + Uygulamanın simgesini değiştirmek için dokunun + Gönder + Gönderinin görünürlüğü + Fotoğraf eklemek için buraya dokunun + Kabul Edilen Biçimler: jpeg, png, gif\n\nMaksimum Dosya Boyutu: 15 MB\n\nAlbümler en fazla 4 fotoğraf veya video içerebilir + Dosya yükle + İsteğe bağlı bir başlık ekleyin + Uygulama, %1$s API\'sinden çok uzun bir hata mesajı aldı + Mesaj önizlemesi + Her mesaja bahsetme ekle + Görüşme indiriliyor + Sıralama ölçütü + Video başlığı + Peertube\'e katıl + 16 yaşından büyüğüm ve bu sunucunun %1$s koşullarını kabul ediyorum + Bağlantılar + Mesajlardaki bağlantıların (URL\'ler, bahsedilmeler, etiketler, vb.) rengini değiştirin + Başlığı tekrar bloglar + Mesajların üst kısmında görünen ad rengini değiştirin + Mesajların üst kısmındaki kullanıcı adının rengini değiştirin + Tekrar bloglamalar için başlığın rengini değiştirin + Gönderiler + Gönderilerin zaman çizelgelerindeki arka plan rengi + Renkleri sıfırla + Tüm özel renklerinizi sıfırlamak için buraya dokunun + Sıfırla + Simgeler + Zaman çizelgelerinin alt kısmındaki simgelerin rengi + Bu etiketi sabitle + Sunucunun logosu + Profili düzenle + Bir eylem yap + Çeviri + Resim önizlemesi + Metin rengi + İçeriklerdeki metin rengini değiştir + Değişiklikleri uygula + Değişiklikleri uygulamak için uygulamayı yeniden başlatmanız gerekiyor + Yeniden Başlat + Kişisel tema kullan + Yukarıda seçilen temanın renklerini değiştirmeye izin ver + Theming + Önce sakla + Tema dışa aktarıldı + Tema CSV dosyası olarak dışa aktarıldı + Ana renk durum çubuğuna uygulansın + Durum çubuğu rengi + Varsayılan temayı geri yükle + Temayı içe aktar + Daha önceden dışa aktarılmış temayı içe aktarmak için dokunun + Temayı dışa aktar + Mevcut temayı dışa aktarmak için dokunun + Tema dosyasını seçerken bir hata oluştu + Tema Seçici + Önceden yüklenmiş bir tema seçin + Temalar + Ana renk gezinme çubuğuna uygulansın + Gezinme çubuğu rengi + Uygulama içeriğinin temel rengi. + Arka plan rengi + Accents select parts of the UI. + Vurgu rengi + Displayed most frequently across your app. + Birincil renk + Yer imlerini sunucuya dışa aktar + Yer imlerini sunucudan içe aktar + Kullanıcı sayısı + Durum sayısı + Sunucu sayısı + Engellendi + %s içinde bitiyor + %s ile gelen yenilikler + You can follow my account for updates + Bu sunucu https://instances.social üzerinde mevcut değil + Tam bağlantıyı görüntüle + Bağlantıyı paylaş + URL panoya kopyalandı + Başka bir uygulamada aç + Yönlendirmeyi kontrol et + Bu bağlantı yeniden yönlendirmiyor + %1$s \n\nredirects to\n\n %2$s + Kullanıcı aracısını değiştir + Özel bir kullanıcı aracısı ayarlayın veya boş bırakın + Allows to customize the user agent used for api calls or with the built-in browser. + UTM parametrelerini kaldır + Uygulama, bir bağlantıyı ziyaret etmeden önce bağlantılardan UTM parametrelerini otomatik olarak kaldıracak. + Trendler + Şu an trendlerde + %d kişi konuşuyor + Twitter hesapları (Nitter üzerinden) + Twitter kullanıcı adları boşlukla ayrılmış + Kimlik kanıtları + Doğrulanmış kimlik + Verified by %1$s (%2$s) + Bildirimi sil + Daha fazla seçenek göster + Bir Pixelfed hikayesidir + Bir ortam dosyası yüklerseniz otomatik olarak Pixelfed hikayenize yüklenecektir. + Ortam dosyası hikayenize başarıyla eklendi! + Eylem devre dışı bırakıldı + Takibi bırak + Bir şeyler ters gitti, lütfen ayarlardan indirme dizininizi kontrol edin. + Duyurular + Duyuru yok! + Tepki ekle + Uygulama içinde favori tarayıcınızı kullanın. Bağlantıları harici olarak açmak için bu özelliğin işaretini kaldırın. + MB cinsinden video önbelleği, sıfır önbellek olmadığı anlamına gelir. + Filigranlar + Resimlerin altına otomatik olarak filigran ekle. Filigran metni her hesap için farklı olabilir. + Dağıtıcı bulunamadı! + Anlık bildirim alabilmeniz için bir dağıtıcı ayarlamalısınız.\nDaha fazla bilgi için: %1$s.\n\nBu mesajı gözardı etmek için ayarlardan anlık bildirimleri kapatabilirsiniz. + Dağıtıcı seçin + diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml new file mode 100644 index 000000000..c8e7ceeec --- /dev/null +++ b/app/src/main/res/values-uk/strings.xml @@ -0,0 +1,1148 @@ + + + Відкрити меню + Закрити меню + Про додаток + Про екземпляр + Політики конфіденційності + Кеш + Вийти + Увійти + + Закрити + Так + Ні + Скасувати + Завантажити + Завантажити %1$s + Збережені медіа-файли + Файл: %1$s + Пароль + Ел. пошта + Облікові записи + Передмухнуте + Мітки + Зберегти + Відновити + Нічого не знайдено! + Екземпляр + Екземпляр: mastodon.social + Тепер працює з обліковим записом %1$s + Додати обліковий запис + Вміст дмуху скопійований в буфер обміну + The URL of the toot has been copied to the clipboard + Змінити + Виберіть зображення… + Очистити + Камера + Видалити все + Перекласти цей дмух. + Запланувати + Розміри тексту та іконок + Змінити поточний розмір тексту: + Змінити поточний розмір піктограм: + Далі + Попередній + Відкрити за допомогою + Перевірити + Медіа-файли + Поділитися через + Розміщено за допомогою Fedilab + Відповіді + Ім\'я користувача + Чернетки + Обране + Нові підписники + Згадки + Передмухи + Показати передмухнуте + Показати відповіді + Відкрити в браузері + Перекласти + Зачекайте кілька секунд, перш ніж розпочати цю дію. + + Домівка + Місцева стрічка + Федеративна стрічка + Параметри + Обране + Зв\'язок + Приглушені користувачі + Заблоковані користувачі + Сповіщення + Запити на підписку + Налаштування + Видалити обліковий запис + Видалити обліковий запис %1$s з застосунку? + Надіслати ел. лист + Натисніть на шлях, щоб змінити його + Не вдалося! + Заплановані дмухи + Наведена нижче інформація може відображати профіль користувача не повністю. + Вставити смайли + Застосунок не збирає emojis на даний момент. + Push notifications + Are you sure you want to logout? + Are you sure you want to logout @%1$s@%2$s? + + Немає повідомлень для відображення + No stories to display + Stories + Передмухнув(-ла) %1$s + Додати цей дмух до обраного? + Видалити цей дмух з обраного? + Передмухнути? + Зняти передмух? + Прикріпити цей дмух? + Відкріпити цей дмух? + Заглушити + Заблокувати + Повідомити про порушення + Видалити + Копіювати + Поділитися + Згадати + Тимчасово заглушити + Видалити & переробити + + Заглушити цей обліковий запис? + Заблокувати цей обліковий запис? + Поскаржитися на цей дмух? + Блокувати цей домен? + Unmute this account? + Unblock this account? + + + Notify + Silent + + + Видалити цей дмух? + Видалити & переробити цей гудок? + + Закладки + Додати в закладки + Видалити закладку + Немає закладок для відображення + Статус був доданий до закладок! + Статус був видалений із закладок! + + %d сек + %d хв + %d год + %d д + + %d second + %d seconds + %d seconds + %d seconds + + + %d minute + %d minutes + %d minutes + %d minutes + + + %d hour + %d hours + %d hours + %d hours + + + %d day + %d days + %d days + %d days + + + Попередження + Що у Вас на думці? + ДМУХ! + QUEET! + CW + Напишіть + Відповісти на дмух + Write a queet + Reply to a queet + Оберіть медіа-файл + Сталася помилка під час вибору медіа-файлу! + Видалити цей медіа-файл? + Ваш дмух пустий! + Видимість дмуху + Видимість дмухів за замовченням: + Повідомлення було відправлено! + Ви відповідаєте на цей дмух: + Чутливий вміст? + + Запостити в публічну стрічку + Не постити в публічну стрічку + Постити тільки тим, хто підписався + Постити тільки до згаданих користувачів + + Чернетки відсутні! + Оберіть дмух + Оберіть обліковий запис + Оберіть деякі облікові записи + Видалити чернетку? + Натисніть, щоб показати оригінальний дмух + Прочитати для людей з вадами зору + + Опис відсутній! + + Реліз %1$s + Розробник: + Ліцензія: + GNU GPLv3 + Сирцевий код: + Переклад дмухів: + Шукати екземпляри: + Дизайнер піктограм: + + Розмова + + Немає облікових записів для відображення + Запит на відмову від підписки + Дмухи \n %1$s + Підписані на \n %1$s + Стежать за вами \n %1$s + Закріплені \n %d + Авторизуватися + Відхилити + + Немає запланованих дмухів для показу! + Дмухніть, а потім оберіть Запланувати з верхнього меню. + Видалити запланований дмух? + Медіа: %d + Дмух було заплановано! + Запланована дата має бути більшою за поточну годину! + Економія батареї увімкнена. Може не працювати, як очікується. + + Час для приглушення має бути більшим за одну хвилину. + %1$s заглушено до %2$s.\n Можете розглушити зі сторінки профілю. + %1$s заглушено до %2$s.\n Натисніть, щоб розглушити. + + Немає сповіщень для показу + згадав вас + wrote a new message + передмухнув ваш статус + додав до обраного ваш статус + підписався(-лась) на вас + asked to follow you + + додати нове сповіщення + додати нове сповіщення + додати нове сповіщення + додати нове сповіщення + + + %d like + %d likes + %d likes + %d likes + + Видалити це сповіщення? + Видалити усі сповіщення? + Сповіщення було видалене! + Усі сповіщення були видалені! + + Підписаний + Стежать за вами + Прикріплено + + Не вдалося отримати ідентифікатор клієнта! + Unable to connect to instance domain! + Немає з\'єднання з Інтернетом! + Обліковий запис заблокований! + Обліковий запис більше не заблоковано! + Обліковий запис приглушено! + Обліковий запис більше не приглушено! + На обліковий запис підписалися! + Ви більше не підписані на обліковий запис! + Передмухнуто! + Більше не передмухнуто! + Дмух додано до обраного! + Дмух видалено з обраного! + На дмух поскаржилися! + Дмух видалено! + Дмух закріплено! + Дмух відкріплено! + Вибачте, виникла помилка! + Сталася помилка! Екземпляр не повернув код авторизації! + Домен екземпляру не валідний! + Сталася помилка під час перемикання облікових записів! + Виникла помилка при пошуку! + Дані профілю збережено! + Ніяка дія не може відбутися + Медіа-файл збережено! + Виникла помилка під час перекладу! + Translations are disabled in settings + Чернетку збережено! + Ви впевнені, що цей екземпляр дозволяє таке число символів? Зазвичай, це 500 символів. + Видимість дмухів змінена для облікового запису %1$s + + Число дмухів за одиницю завантаження + Завжди + Wi-Fi + Запитати + Завантажити медіа-файл + Завантажити зображення + Показати більше… + Згорнути… + Чутливий вміст + Вимкнути GIF аватари + Шлях: + Зберігати чернетки автоматично + Додати URL медіа-файлу до дмуху + Сповіщати, якщо хтось підписується + Сповіщати, якщо хтось передмухнув ваш статус + Сповіщати, якщо хтось додав статус до обраного + Сповіщати, якщо хтось згадує вас + Notify when a poll ended + Notify for new posts + Показати діалогове вікно підтвердження перед тим, як передмухнути + Показати діалогове вікно підтвердження перед тим, як додати до обраного + Повідомити тільки при Wi-Fi + Сповістити? + Тихі сповіщення + NSFW тимчасовий (секунди, 0 означає off) + Media Description timeout (seconds, 0 means off) + Редагувати обліковий запис + Custom sharing + Your custom sharing URL… + Біо… + Блокування облікового запису + Зберегти зміни + Вибрати малюнок обкладинки + Перегляд зображень + Автоматично розбивати гудки довші 500 символів + Ви досягли ліміту в 160 символів! + Ви досягли ліміту в 30 символів! + Між + і + Інтервал має бути більшим за %1$s + Інтервал має бути меншим за %1$s + Час початку + Час закінчення + Через вбудований браузер + Кастомні вкладки + Увімкнути JavaScript + Автоматично розкривати Cw + Приймати куки від сторонніх сайтів + Ваш API ключ, можете залишити пустим для Яндекс + + Темна + Світла + Чорна + + Встановити колір LED: + + Синій + Блакитний + Маджента + Зелений + Червоний + Жовтий + Білий + + Підписатися + Розблокувати + Заглушити + Скасувати \"ігнор\" + Запит надіслано + Підписався(-лась) на вас + Пошук + Починати відповіді з великої букви + Змінити розмір зображень + Resize videos + + Push-сповіщення + Будь ласка, потрібно підтвердити отримання push-сповіщень. Ви можете змінити налаштування пізніше (вкладка сповіщень). + + Очистити кеш + Є %1$s даних в кеші..\n\n Хотіли б видалити їх? + Мб + Кеш було очищено! %1$s було звільнено + + Title + Title… + Description + Keywords + Keywords… + + Синхронізувати + Фільтрувати + Ваші дмухи + Your notifications + Загальнодоступне + Поза списком + Приватнi + Особисті + Деякі ключові слова… + Показати всі медіа + Показати закріплені + Відповідних повідомлень не знайдено! + Резервне копіювання дмухів для %1$s + %1$s нових дмухів імпортовано + %1$s new notifications have been imported + + Dates descending + Dates ascending + + + Ні + Тільки + Обидва + + Жодного дмуху не знайдено в базі даних. Будь ласка, використовуйте кнопку синхронізації в меню, щоб отримати їх. + + Записані дані + Тільки основна інформація з облікових записів зберігається на пристрої. Ці дані залишаються суворо конфіденційними і можуть використовуватися лише додатком. У разі видалення додаток одразу видаляє ці дані. \n + ⚠ Логіни та паролі не зберігаються. Вони використовуються тільки під час безпечної аутентифікації (SSL) на екземплярі. + + Дозволи: + - ACCESS_NETWORK_STATE: використовується для виявлення, чи пристрій підключено до Wi-Fi мережі. \n - Інтернет: використовується для запитів до до екземпляра. \n - WRITE_EXTERNAL_STORAGE: використовується для зберігання медіа або переміщення програми на карту SD \n - READ_EXTERNAL_STORAGE: використовується для додавання медіа до тутів. \n - BOOT_COMPLETED: використовується для запуску служби сповіщень. \n - WAKE_LOCK: Використовується під час роботи служби сповіщень. + + API дозволи: + - Читати: Читати дані.\n + - Писати: Постити статуси і завантажувати медіа-файли.\n + - Підписка: Підписка, відписка, блок, розблок.\n\n + ⚠ Ці дії відбуваються тільки за запитом користувача. + + Бібліотеки + Додаток не відслідковує користувача І не містить реклами.\n\n + Використання сторонніх бібліотек мінімізоване: \n + - Glide: для управління медіа-файлами\n + - Android-Job: Для управління сервісами\n + - PhotoView: Для управління зображеннями\n + + Переклад дмухів + Додаток перекладає дмухи через Yandex API.\n + Умови конфіденційності Yandex можна знайти за посиланням: https://yandex.ru/legal/confidential/?lang=en + + Подяки: + Відфільтруйте регулярним виразом + Пошук + Видалити + Отримати більше дмухів… + + Списки + Ви дійсно бажаєте остаточно видалити цей список? + Список пустий. Коли додані до списку почнуть писати, їх статуси з\'являться у списку. + Додати до списку + Додати список + Видалити список + Редагувати список + Нова назва списку + The account was added to the list! + You don\'t have any lists yet! + + %1$s відправлено до %2$s + Автентифікація не працює? + Деякі поради, що можуть допомогти:\n\n + - Перевірте правильність написання назви екземпляру\n\n + - Перевірте, чи екземпляр працює\n\n + - Якщо використовуєте двофакторну авторизацію (2FA), будь ласка скористайтеся посиланням внизу, коли назву екземпляру заповнено\n\n + - Також можна використовувати посилання без 2FA\n\n + - Якщо все одно не працює, повідомте нам на Framagit: https://framagit.org/tom79/fedilab/issues + + Медіа-файл завантажений. Натисніть щоб відобразити. + Ця дія довготривала, ви отримаєте сповіщення після закінчення. + Почекайте, будь ласка… + Експорт статусів + Експорт статусів для %1$s + %1$s дмухів з %2$s були експортовані. + Щось пройшло не так під час експорту даних з %1$s + Something went wrong when exporting data! + Something went wrong when importing data! + + Проксі + Увімкнути проксі? + Вузол + Порт + Логін + Пароль + Додати деталі гудку при поділитися + Підтримати додаток на Liberapay + Помилка у регулярному виразі! + No timelines was found on this instance! + Видалити цей екземпляр? + Перекласти + Follow instance + You already follow this instance! + The instance is followed! + Партнерство + Інформація + Hide boosts from %s + Feature on profile + Show boosts from %s + Don\'t feature on profile + The account is now featured on profile + The account is no longer featured on profile + Boosts are now shown! + Boosts are now hidden! + Пряме повідомлення + Фільтри + No filters to display. You can create one by tapping on the \"+\" button. + Ключове слово або фраза + Домашня стрічка + Публічна стрічка + Сповіщення + Розмови + Will be matched regardless of casing in text or content warning of a toot + Drop instead of hide + Filtered toots will disappear irreversibly, even if filter is later removed + When the keyword or phrase is alphanumeric only, it will only be applied if it matches the whole word + Слово повністю + Фільтр контексту + One or multiple contexts where the filter should apply + Закінчується після + Видалити фільтр? + Оновлення фільтра + Створити фільтр + На кого підписатися + There is no accounts listed for the moment! + Підписатися + Обрати все + Зняти виділення + %s is followed! + Creating the list %s + Adding accounts to the list + Accounts were added to the list + Adding accounts to the list + You have not created a list yet. Tap on the \"+\" button to add a new one. + На кого підписатися + Trunk API + Account(s) can\'t be followed + Fetching remote account + Automatically expand hidden media + New follow + New Boost + New Favourite + New Mention + Poll Ended + Новий гудок + Toots Backup + New posts + Медіа завантаження + Змінити звук сповіщення + Оберіть сигнал + Увімкнути часовий слот + Практичні відео + Завантаження віддаленого потоку! + Немає заблокованих доменів! + Розблокувати домен + Ви впевнені, що хочете розблокувати %s? + Ви впевнені, що хочете заблокувати %s? + Заблоковані домени + Блокувати домен + Домен заблокований + Домен більше не заблоковано! + Завантаження віддаленого статусу + Коментувати + Peertube екземпляр + Будьте першими, хто залишив коментар до цього відео, використовуючи праву верхню кнопку! + %s переглядів + Тривалість: %s + Додати екземпляр + Коментарі не ввімкнуто до відео! + Забрати роздільну здатність + Peertube обране + Це відео було додане до закладок! + Це відео було видалено із закладок! + У ваших улюблених немає відео Peertube! + Канал + Відео + Канали + Use Emoji One + Information + Display previews in all toots + New UX/UI designer + Display video previews + The account id has been copied in the clipboard! + Change the language + Default language + Truncate long toots + Truncate toots over \'x\' lines. Zero means disabled. + Display more + Display less + Manage tags + The tag already exists! + The tag has been stored! + The tag has been changed! + The tag has been removed! + Schedule boost + The boost is scheduled! + No scheduled boost to display! + Schedule boost.]]> + Art timeline + Open menu + Go back + Logo of the application + Profile picture + Profile banner + Contact admin of the instance + Add new + MastoHost logo + Emoji picker + Оновити + Expand the conversation + Remove an account + Remove the blocked domain + Custom emoji picker + Play video + New toot + Image of the card + Hide media + Favicon + Add description for media (for the visually impaired) + + Ніколи + 30 хвилин + 1 година + 6 годин + 12 годин + 1 день + 1 тиждень + + In this field, you need to write your instance host name.\nFor example, if you created your account on https://mastodon.social\nJust write mastodon.social (without https://)\n + You can start writing first letters and names will be suggested.\n\n + ⚠ The Login button will only work if the instance name is valid and the instance is up! + + More information + + Мови + Media only + Show NSFW + Crowdin translations + Crowdin manager + Translation of the application + About Crowdin + Bot + Pixelfed instance + Mastodon instance + Any of these + All of these + None of these + Any of these words (space-separated) + All these words (space-separated) + Add some words to filter (space-separated) + Change column name + Misskey instance + No app supporting this link is installed on your device. + Subscriptions + Overview + Trending + Recently added + Local + Upload + Reply + Delete a comment + Are you sure to delete this comment? + Full screen video + Mode for videos + Select the file to upload + My videos + Title + License + Category + Language + This video contains mature or explicit content + Enable video comments + Update video + Description + The video has been updated! + Upload cancelled! + The video has been uploaded! + Uploading, please wait… + Tap here to edit the video data. + Delete video + Are you sure to delete this video? + Display NSFW videos + No videos to display! + Leave a comment + Share + Choose a schedule mode + From device + From server + Toots (Server) + Toots (Device) + Modify + Display new toots above the \"Fetch more\" button + Timelines + Interface + Contacts + %1$s commented your video %2$s]]> + %1$s is following your channel %2$s]]> + %1$s is following your account]]> + %1$s has been published]]> + %1$s succeeded]]> + %1$s failed]]> + %1$s published a new video: %2$s]]> + %1$s has been blacklisted]]> + %1$s has been unblacklisted]]> + Export data + Import Data + Select the file to import + An error occurred when selecting the backup file! + Add a public comment + Send comment + There is no Internet connection. Your message has been stored in drafts. + Plain text + HTML + Markdown + Logout account + All + Support the app + Open Collective enables groups to quickly set up a collective, raise funds and manage them transparently. + Copy link + Connect + Normal + Compact + Console + Set display mode + Patch the Security Provider + Update tracking domains + The tracking data base has been updated! + http calls blocked by the application + List of blocked calls + Submit + The data base has been exported! + Featured hashtags + Filter timeline with tags + No tags + Hide the \"delete\" button in the notification tab + Attach an image when sharing a URL + + Poll + Polls + Create a poll + Choice 1 + Choice 2 + Choice %d + You need two choices at least for the poll! + Done + end at %s + Refresh poll + Vote + A poll you have voted in has ended + A poll you tooted has ended + Customize + Categories + Time slot + Advanced + Display \'new\' badge on unread toots + Peertube + Move timeline + Hide timeline + Reorder timelines + List permanently deleted + Followed instance removed + Pinned tag removed + Undo + You need to keep two visible tabs! + Reorder timelines + Main timelines can only be hidden! + BBCode + Always mark media as sensitive + GNU instance + Cached status + Forward tags in replies + Long press to store media + Blur sensitive media + Display timelines in a list + Display timelines + Mark bot accounts in toots + Manage tags + Remember the position in Home timeline + History + Playlists + Display name + You don\'t have any playlists. Tap on the \"+\" icon to add a new playlist + You must provide a display name! + The channel is required when the playlist is public. + Create a playlist + There is nothing in this playlist yet. + redo + Gallery + Emoji + Sticker + Eraser + Text + Filter + Brush + Are you sure you want to exit without saving the image? + Discard + Saving… + Image Saved Successfully! + Failed to save Image + Opacity + Enable photo editor + Add a poll item + Remove last poll item + Mute conversation + Unmute conversation + The conversation is no longer muted! + The conversation is muted + Open application features + Timed mute + Mention the account + Refresh cache + Mention the status + News + General + Regional + Art + Journalism + Activism + Gaming + Technology + Adult content + Furry + Food + Logo of the instance + Something went wrong when checking available instances! + Join Mastodon + Choose an instance by picking up a category, then tap on a check button. + Choose an instance by tapping on a check button. + %1$s users + Confirm password + I agree to %1$s and %2$s + server rules + terms of service + Sign up + This instance works with invitations. Your account will need to be manually approved by an administrator before being usable. + Please, fill all the fields! + Passwords don\'t match! + The email doesn\'t seem to be valid! + Your username will be unique on %1$s + You will be sent a confirmation e-mail + Use at least 8 characters + Password should contain at least 8 characters + Username should only contain letters, numbers and underscores + Account created! + Your account has been created!\n\n + Think to validate your email within the 48 next hours.\n\n + You can now connect your account by writing %1$s in the first field and click on Connect.\n\n + Important: If your instance required validation, you will receive an email once it is validated! + + Save the message in drafts? + Administration + Reports + No reports to display! + Reconnect the account + The application failed to access the admin features. You might need to reconnect the account for having the correct scope. + Unresolved + Remote + Active + Pending + Disabled + Silenced + Suspended + Permissions + Email status + Login status + Joined + Most recent IP + Warn + Disable + Silence + Notify the user per e-mail + Custom warning + User + Moderator + Administrator + Confirmed + Not confirmed + Reported statuses + Account + Undo silence + Undo disable + Suspend + Undo suspend + The account is silenced! + The account is no longer silenced! + The account is suspended! + The account is no longer suspended! + The account is disabled! + The account is no longer disabled! + The account has been warned! + Display the admin menu + Display the admin feature in statuses + Allow + The account is approved! + The account is rejected! + Assign to me + Unassign + Mark as resolved + Mark as unresolved + Empty content! + Display Fedilab features button + The application needs to access audio recording + Voice message + Enable quick reply + The account you are replying might not see your message! + If disabled, the app will always load last statuses + If disabled, sensitive media will be hidden with a button + Store media in full size with a long press on previews + Add an ellipse button at the top right for listing all tags/instances/lists + During the time slot, the app will send notifications. You can reverse (ie: silent) this time slot with the right spinner. + Display a Fedilab button below profile picture. It is a shortcut for accessing in-app features. + Allow to reply directly in timelines below statuses + Previews will not be cropped in timelines + Allow to play embedded videos directly in timelines + Allow to reverse the way to read statuses that are displayed once tapping the fetch more button + This option allows to support recent cipher suites. It is useful for older Android devices or if you cannot connect to your instance. + Exclusively for Peertube videos. Switch this mode if you cannot play them. + These tags will allow to filter statuses from profiles. You will have to use the context menu for seeing them. + Automatically insert a line break after the mention to capitalize the first letter + Allow content creators to share statuses to their RSS feeds + Compose + Maximum retry times when uploading media + Create a new Folder here + Enter the folder name + Please enter a valid folder name + This folder already exists.\n Please provide another name for the folder + Select + Default Directory + Folder + Create folder + Display a toast message after an action has been completed (boost, fav, etc.)? + Muted instances have been exported! + Add an instance + Export instances + Import instances + Crash reports + Enable crash reports + If enabled, a crash report will be created locally and then you will be able to share it. + Fedilab зупинився :( + Ви можете надіслати мені звіт про збій електронною поштою. Це допоможе виправити це:)\n\nВи можете додати додатковий вміст. Дякую! + Use the wysiwyg + When enabled, you will be able to format your text easily with tools. + Statistics + Total statuses + Number of boosts + Number of favourites + Number of mentions + Number of follows + Number of polls + Number of replies + Number of statuses + Statuses + Visibility + Number with media + Number with sensitive media + Number with CW + First status date + Last status date + First notification date + Last notification date + Frequency + %s statuses per day + %s notifications per day + Date range + Groups + No groups! + Disable custom animated emojis + Charts + Display charts + The application collects your local data, please wait… + Backup + Auto backup statuses + This option is per account. It will launch a service that will automatically store your statuses locally in the database. That allows to get statistics and charts + Auto backup notifications + This option is per account. It will launch a service that will automatically store your notifications locally in the database. That allows to get statistics and charts + Report account + Send an invitation + Your instance does not allow to register a new account! + + %d vote + %d votes + %d votes + %d votes + + + %d voter + %d voters + %d voters + %d voters + + + Single choice + Multiple choices + + + 5 minutes + 30 minutes + 1 hour + 6 hours + 1 day + 3 days + 7 days + + + Webview + Direct stream + + For joining my instance \"%1$s\", you can download Fedilab:\n\nF-Droid: %2$s\nGoogle: %3$s\n\nThen open the link below with Fedilab and create your account :)\n\n%4$s + + Your poll can\'t have duplicated options! + For all accounts + Database cache + Clear your home timeline cache + Clear your cached statuses + Clear your bookmarks + Files in cache + Total notifications + Hide menu items + Fedilab is running live notifications + For %1$s accounts with %2$s events + Live notifications for %1$s + Live notifications will be enabled for this account. + Clear cache when leaving + The cache (media, cached messages, data from the built-in browser) will be automatically cleared when leaving the application. + Do you want to unfollow this account? + Show confirmation dialog before unfollowing + Replace Youtube with Invidio.us + Invidious is an alternative front-end to YouTube + Enter your custom host or leave blank for using invidious.snopyta.org + Replace Twitter with Nitter + Nitter is an open source alternative Twitter front-end focused on privacy. + Enter your custom host or leave blank for using nitter.net + Replace Instagram with Bibliogram + Bibliogram is an open source alternative Instagram front-end focused on privacy. + Enter your custom host or leave blank for using bibliogram.art + Replace Reddit with Libreddit + Libreddit is an open source alternative Reddit front-end focused on privacy. + Enter your custom host or leave blank for using libredd.it + Replace Medium links + Replace medium.com links with an open source alternative front-end focused on privacy. + Default: scribe.rip + Replace Wikipedia links + Replace Wikipedia link with an open source alternative front-end focused on privacy. + Default: wikiless.org + Hide Fedilab notification bar + For hiding the remaining notification in the status bar, tap on the eye icon button then uncheck: \"Display in status bar\" + Use a push notifications system for getting notifications in real time. + No live notifications + Live notifications + Notifications will be fetched every 15 minutes. + Add notes + Notes for the account + Allow to compress large photos into smaller sized photos with very less or negligible loss in quality of the image. + Allow compressing videos while maintaining their quality. + The app is compressing the media, it can be quite long… + Change app icon + Tap to change the app icon + Post + Visibility of the post + Tap here to add photos + Accepted Formats: jpeg, png, gif \n\nMax File Size: 15 MB \n\nAlbums can contain up to 4 photos or videos + Upload media + Add an optional caption + The app received a very long error message from the API %1$s + Message preview + Add mentions in each message + Fetching conversation + Order by + Title for the video + Join Peertube + I am at least 16 years old and agree to the %1$s of this instance + Links + Change the color of links (URLs, mentions, tags, etc.) in messages + Reblogs header + Change the color of display name at the top of messages + Change the color of the user name at the top of messages + Change the color of the header for reblogs + Posts + Background color of posts in timelines + Reset colors + Tap here to reset all your custom colors + Reset + Icons + Color of bottom icons in timelines + Pin this tag + Logo of the instance + Edit profile + Make an action + Translation + Image preview + Text color + Change the text color in pots + Apply changes + You need to restart the application to apply changes + Restart + Use a custom theme + Allow to override colors of the selected theme above + Theming + Store before + The theme was exported + The theme has been successfully exported in CSV + Apply the primary color to the status bar + Status bar color + Restore a default theme + Import a theme + Tap here to import a theme from a previous export + Export the theme + Tap here to export the current theme + An error occurred when selecting the theme file + Theme Picker + Select a pre-installed theme + Themes + Apply the primary color to the navigation bar + Navigation bar color + The underlying color of the app’s content. + Background color + Accents select parts of the UI. + Accent color + Displayed most frequently across your app. + Primary color + Export bookmarks to the instance + Import bookmarks from the instance + User count + Status count + Instance count + Blocked + End in %s + What\'s new in %s + You can follow my account for updates + This instance is not available on https://instances.social + Display full link + Share link + The URL has been copied to the clipboard + Open with another app + Check redirect + This URL does not redirect + %1$s \n\nredirects to\n\n %2$s + Change the user agent + Set a custom user agent or leave blank + Allows to customize the user agent used for api calls or with the built-in browser. + Remove UTM parameters + The app will automatically remove UTM parameters from URLs before visiting a link. + Trends + Trending now + %d people talking + Twitter accounts (via Nitter) + Twitter usernames space separated + Identity proofs + Verified identity + Verified by %1$s (%2$s) + Delete the notification + Display more options + It is a Pixelfed story + Upload a media, it will be automatically added to your Pixelfed story. + Media successfully added to your story! + Action disabled + Unfollow + Something went wrong, please check your download directory in settings. + Announcements + No announcements! + Add a reaction + Use your favourite browser inside the app. Uncheck this feature to open links externally. + Video cache in MB, zero means no cache. + Watermarks + Automatically add a watermark at the bottom of pictures. The text can be customized for each account. + No distributors found! + You need a distributor for receiving push notifications.\nYou will find more details at %1$s.\n\nYou can also disable push notifications in settings for ignoring that message. + Select a distributor + diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml new file mode 100644 index 000000000..cd68ab854 --- /dev/null +++ b/app/src/main/res/values-vi/strings.xml @@ -0,0 +1,1131 @@ + + + Mở trình đơn + Đóng trình đơn + Trong khoảng + Về ví dụ + Riêng tư + Chổ dấu + Đăng xuất + Đăng nhập + + Gần + Vâng + Không + Hủy + Tải về + Tải về %1$s + Đã lưu phương tiện + Tập tin: %1$s + Mật khẩu + E-mail + Tài khoản + Thổi còi + Thẻ + Lưu + Khôi phục + Ko có kết quả! + Ví dụ + Ví dụ: mastodon.social + Bây giờ làm việc với tài khoản %1$s + Thêm một tài khoản + Nội dung của thổi còi đã được sao chép vào khay nhớ tạm + The URL of the toot has been copied to the clipboard + Thay đổi + Chọn ảnh… + Dọn dẹp + Camera + Xóa hết + Dịch này thổi còi. + Lịch trình + Kích thước văn bản và biểu tượng + Thay đổi kích thước văn bản hiện tại: + Thay đổi kích thước biểu tượng hiện tại: + Kế tiếp + Trước + Mở với + Xác thực + Phương tiện truyền thông + Chia sẽ với + Chia sẻ qua Fedilab + Trả lời + Tên người dùng + Bản nháp + Yêu thích + Người theo dõi mới + Đề cập đến + Tăng + Hiển thị tăng + Hiển thị bài trả lời + Mở trong trình duyệt + Dịch + Vui lòng chờ vài giây trước khi thực hiện hành động này. + + Nhà + Thời hạn địa phương + Lịch trình liên kết + Tùy chọn + Yêu thích + Giao tiếp + Người dùng bị tắt tiếng + Người dùng bị chặn + Thông báo + Theo yêu cầu + Cài đặt + Xóa tài khoản + Xóa tài khoản %1$s khỏi ứng dụng? + Gửi email + Nhấp vào đường dẫn để thay đổi nó + Thất bại! + Các thổi còi theo lịch + Thông tin dưới đây có thể phản ánh hồ sơ của người dùng không đầy đủ. + Chèn eoji + Các ứng dụng đã không thu thập emojis tùy chỉnh cho thời điểm này. + Push notifications + Are you sure you want to logout? + Are you sure you want to logout @%1$s@%2$s? + + Không thổi còi để hiển thị + No stories to display + Stories + Được thúc đẩy bởi %1$s + Thêm cái còi này vào mục yêu thích của bạn? + Loại bỏ thổi còi này khỏi mục yêu thích của bạn? + Tăng thổi còi này? + Tháo nốt nhạc này? + Pin này thổi còi? + Hủy bỏ cái thổi còi này? + Tắt tiếng + Súc cây + Báo cáo + Tẩy + Sao chép + Chia sẻ + Ghi chép + Timed mute + Delete & re-draft + + Ẩn tài khoản này? + Chặn tài khoản này? + Báo cáo này thổi còi? + Block this domain? + Unmute this account? + Unblock this account? + + + Notify + Silent + + + Hủy bỏ thổi còi này? + Delete & re-draft this toot? + + Bookmarks + Add to bookmarks + Remove bookmark + No bookmarks to display + Status has been added to bookmarks! + Status was removed from bookmarks! + + %d s + %d m + %d h + %d d + + %d seconds + + + %d minutes + + + %d hours + + + %d days + + + Cảnh báo + Bạn đang nghĩ cái gì thế? + Thổi còi! + QUEET! + cw + Viết một cái còi + Trả lời thổi còi + Write a queet + Reply to a queet + Chọn phương tiện + Đã xảy ra lỗi khi chọn phương tiện! + Xóa phương tiện này? + Thổi còi của bạn trống! + Tầm nhìn của thổi còi + Khả năng hiển thị của các toots theo mặc định: + Thổi còi đã được gửi! + Bạn đang trả lời cho thổi còi này: + Nội dung nhạy cảm? + + Đăng lên các mốc công khai + Không đăng lên các dòng thời gian công khai + Đăng lên người theo dõi chỉ + Đăng lên chỉ những người dùng được đề cập + + Không có bản nháp! + Chọn một cái còi + Chọn một tài khoản + Chọn một số tài khoản + Xóa dự thảo? + Nhấp chuột vào nút để hiển thị các thổi còi ban đầu + Mô tả cho người khiếm thị + + Không có mô tả nào! + + Phát hành %1$s + Nhà phát triển: + Giấy phép: + GNU GPL V3 + Mã nguồn: + Dịch của thổi còi: + Trường hợp tìm kiếm: + Người thiết kế biểu tượng: + + Cuộc hội thoại + + Không có tài khoản để hiển thị + Không theo yêu cầu + Thổi còi\n %1$s + Tiếp theo\n %1$s + Người theo dõi \n %1$s + Đã ghim \n %d + Ủy quyền + Từ chối + + Không có lịch trình thổi còi để hiển thị! + Viết toot và sau đó chọn Lịch từ trình đơn trên cùng. + Xóa nhịp cầu định kỳ? + Phương tiện truyền thông: %d + Các thổi còi đã được lên kế hoạch! + Ngày dự kiến ​​phải lớn hơn giờ hiện tại! + Trình tiết kiệm pin đã được kích hoạt! Nó có thể không hoạt động như mong đợi. + + The time for muting should be greater than one minute. + %1$s has been muted until %2$s.\n You can unmute this account from their profile page. + %1$s is muted until %2$s.\n Tap here to unmute the account. + + Không có thông báo để hiển thị + đề cập đến bạn + wrote a new message + đẩy mạnh tình trạng của bạn + ưu đãi tình trạng của bạn + đã theo bạn + asked to follow you + + và một thông báo khác +và %d thông báo khác + + + %d likes + + Xóa thông báo? + Xóa tất cả các thông báo? + Thông báo đã bị xóa! + Tất cả các thông báo đã bị xóa! + + Tiếp theo + Người theo dõi + Đã ghim + + Không thể lấy id khách hàng! + Unable to connect to instance domain! + Không có kết nối Internet! + Tài khoản đã bị chặn! + Tài khoản không còn bị chặn! + Tài khoản đã bị tắt tiếng! + Tài khoản không còn bị tắt tiếng nữa! + Tài khoản đã được theo sau! + Tài khoản không còn theo sau! + Thổi còi đã được đẩy mạnh! + Thổi còi không còn được tăng cường! + Các thổi còi đã được thêm vào mục yêu thích của bạn! + Các thổi còi đã được gỡ bỏ khỏi mục yêu thích của bạn! + Các thổi còi đã được báo cáo! + Các thổi còi đã bị xóa! + Thổi còi đã được gắn! + Cái thổi còi bị bỏng! + Rất tiếc! Đã xảy ra lỗi! + Đã xảy ra lỗi! Trường hợp đã không trả lại mã ủy quyền! + Miền dụ không có vẻ là valide! + Đã xảy ra lỗi khi chuyển đổi giữa các tài khoản! + Đã xảy ra lỗi khi tìm kiếm! + Dữ liệu tiểu sử đã được lưu! + Không thể thực hiện hành động + Các phương tiện truyền thông đã được cứu! + Đã xảy ra lỗi khi dịch! + Translations are disabled in settings + Bản nháp đã được lưu! + Bạn có chắc là trường hợp này cho phép số nhân vật này? Thông thường, giá trị này gần 500 ký tự. + Tầm nhìn của các toots đã được thay đổi cho tài khoản %1$s + + Số lượng các toots trên mỗi lần tải + Luôn luôn + WIFI + Hỏi + Nạp vật liệu in + Tải hình ảnh + Cho xem nhiều hơn… + Show less… + Nội dung nhạy cảm + Tắt hình đại diện GIF + Con đường: + Tự động lưu dự thảo + Thêm URL của phương tiện truyền thông vào các toots + Thông báo khi ai đó theo bạn + Thông báo khi ai đó tăng tình trạng của bạn + Thông báo khi ai đó yêu thích trạng thái của bạn + Thông báo khi ai đó đề cập đến bạn + Notify when a poll ended + Notify for new posts + Hiển thị hộp thoại xác nhận trước khi tăng + Hiển thị hộp thoại xác nhận trước khi thêm vào mục yêu thích + Chỉ thông báo bằng WIFI + Thông báo? + Thông báo im lặng + Thời gian chờ của NSFW (giây, 0 có nghĩa là tắt) + Media Description timeout (seconds, 0 means off) + Chỉnh sửa tiểu sử + Custom sharing + Your custom sharing URL… + Tiểu sử… + Lock account + Lưu thay đổi + Chọn một hình ảnh tiêu đề + Fit preview images + Automatically split toots in replies when chars are over: + Bạn đã đạt đến 160 ký tự cho phép! + Bạn đã đạt đến 30 ký tự! + Giữa + + Thời gian phải lớn hơn %1$s + Thời gian phải thấp hơn %1$s + Thời gian bắt đầu + Thời gian kết thúc + Sử dụng trình duyệt tích hợp + Custom tabs + Bật Javascript + Automatically expand cw + Cho phép cookie của bên thứ ba + Your API key, you can leave blank for Yandex + + Dark + Light + Black + + Đặt màu LED: + + Màu xanh da trời + Màu lục lam + Màu đỏ tươi + Màu xanh lá + Đỏ + Màu vàng + Trắng + + Theo + Mở khóa + Tắt tiếng + Bật tiếng + Đã gửi yêu cầu + Sau bạn + Tìm kiếm + First letter in capital for replies + Resize pictures + Resize videos + + Push thông báo + Vui lòng xác nhận thông báo đẩy mà bạn muốn nhận. + Bạn có thể bật hoặc tắt các thông báo này sau trong cài đặt (tab Thông báo). + + + Xóa bộ nhớ chổ dấu + Có %1$s dữ liệu trong bộ nhớ cache.\n\nBạn có muốn xóa chúng? + Mb + Cache đã bị xóa! %1$s đã được phát hành + + Title + Title… + Description + Keywords + Keywords… + + Synchronize + Filter + Your toots + Your notifications + Public + Unlisted + Private + Direct + Some keywords… + Show media + Show pinned + No matching result found! + Backup toots for %1$s + %1$s new toots have been imported + %1$s new notifications have been imported + + Dates descending + Dates ascending + + + No + Only + Both + + No toots were found in database. Please, use the synchronize button from the menu to retrieve them. + + Dữ liệu đã ghi + Chỉ có thông tin cơ bản từ các tài khoản được lưu trữ trên thiết bị. + Những dữ liệu này là bí mật và chỉ có thể được sử dụng bởi các ứng dụng. + Xóa ứng dụng sẽ xóa ngay các dữ liệu này.\n + ⚠ Đăng nhập và mật khẩu không bao giờ được lưu trữ. Chúng chỉ được sử dụng trong quá trình xác thực an toàn (SSL) với một cá thể. + + Quyền: + -ACCESS_NETWORK_STATE: Được dùng để phát hiện nếu thiết bị được kết nối với mạng WIFI.\n +         -INTERNET: Được sử dụng cho truy vấn đến một cá thể.\n +         -WRITE_EXTERNAL_STORAGE: Được sử dụng để lưu trữ phương tiện hoặc để di chuyển ứng dụng trên thẻ SD.\n +         -READ_EXTERNAL_STORAGE: Được sử dụng để thêm phương tiện vào toots.\n +         -BOOT_COMPLETED: Được sử dụng để bắt đầu dịch vụ thông báo.\n +         -WAKE_LOCK: Được sử dụng trong dịch vụ thông báo. + Quyền của API: + -Đọc: Đọc dữ liệu.\n +         -Viết: Đăng trạng thái và phương tiện tải lên cho các trạng thái.\n +         - Thực hiện theo: Theo dõi, hủy theo dõi, chặn, bỏ chặn.\n\n +         ⚠ Những hành động này chỉ được thực hiện khi người dùng yêu cầu họ. + Theo dõi và Thư viện + Ứng dụngkhông sử dụng công cụ theo dõi(đo lường đối tượng, báo cáo lỗi, v. v.) và không chứa bất kỳ quảng cáo nào.\n\n +         Việc sử dụng thư viện được giảm thiểu:\n +         -Glide : Để quản lý phương tiện truyền thông\n +         -Android-Job: Để quản lý các dịch vụ\n +         -PhotoView: Để quản lý hình ảnh\n + Dịch thổi còi + Ứng dụng cung cấp khả năng dịch toots bằng cách sử dụng miền địa phương của thiết bị và API Yandex.\n + Yandex có chính sách riêng tư phù hợp của nó có thể được tìm thấy ở đây: https://yandex.ru/legal/confidential/?lang=en + + Cảm ơn bạn: + Lọc theo biểu thức thông thường + Tìm kiếm + Xóa bỏ + Tìm nạp thêm thổi còi… + + Danh sách + Bạn có chắc chắn muốn xóa vĩnh viễn danh sách này? + Không có gì trong danh sách này. Khi các thành viên của danh sách này đăng các trạng thái mới, chúng sẽ xuất hiện ở đây. + Thêm vào danh sách + Thêm vào danh sách + Xoá danh sách + Chỉnh sửa danh sách + Tiêu đề danh sách mới + The account was added to the list! + You don\'t have any lists yet! + + %1$s đã chuyển sang %2$s + Xác thực không hoạt động? + Dưới đây là một số kiểm tra có thể giúp:\n\n + - Kiểm tra không có lỗi chính tả trong tên dụ\n\n + - Kiểm tra rằng trường hợp của bạn không xuống\n\n + - Nếu bạn sử dụng xác thực hai yếu tố (2FA), vui lòng sử dụng liên kết ở cuối (khi tên cá thể được điền)\n\n + - Bạn cũng có thể sử dụng liên kết này mà không sử dụng 2FA\n\n + - Nếu vẫn không hoạt động, vui lòng nêu lên một vấn đề về Framagit tại https://framagit.org/tom79/fedilab/issues + + Media has been loaded. Tap here to display it. + This action can be quite long. You will be notified when it will be finished. + Still running, please wait… + Export statuses + Export statuses for %1$s + %1$s toots out of %2$s have been exported. + Something went wrong when exporting data for %1$s + Something went wrong when exporting data! + Something went wrong when importing data! + + Proxy + Enable proxy? + Host + Port + Login + Mật khẩu + Add toot details when sharing + Support the app on Liberapay + There is an error in the regular expression! + No timelines was found on this instance! + Delete this instance? + Translate in + Follow instance + You already follow this instance! + The instance is followed! + Partnerships + Thông tin + Hide boosts from %s + Feature on profile + Show boosts from %s + Don\'t feature on profile + The account is now featured on profile + The account is no longer featured on profile + Boosts are now shown! + Boosts are now hidden! + Direct message + Filters + No filters to display. You can create one by tapping on the \"+\" button. + Keyword or phrase + Home timeline + Public timelines + Notifications + Conversations + Will be matched regardless of casing in text or content warning of a toot + Drop instead of hide + Filtered toots will disappear irreversibly, even if filter is later removed + When the keyword or phrase is alphanumeric only, it will only be applied if it matches the whole word + Whole word + Filter contexts + One or multiple contexts where the filter should apply + Expire after + Delete filter? + Update filter + Create filter + Whom to follow + There is no accounts listed for the moment! + Follow + Select all + Unselect all + %s is followed! + Creating the list %s + Adding accounts to the list + Accounts were added to the list + Adding accounts to the list + You have not created a list yet. Tap on the \"+\" button to add a new one. + Who to follow + Trunk API + Account(s) can\'t be followed + Fetching remote account + Automatically expand hidden media + New follow + New Boost + New Favourite + New Mention + Poll Ended + New Toot + Toots Backup + New posts + Media Download + Change notification sound + Select Tone + Enable time slot + How To Videos + Fetching remote thread! + No blocked domains! + Unblock domain + Are you sure to unblock %s? + Are you sure to block %s?\n\nYou will not see any content from that domain in any public timeline or in your notifications. Your followers from that domain will be removed. + Blocked domains + Block domain + The domain is blocked + The domain is no longer blocked! + Fetching remote status + Comment + Peertube instance + Be the first to leave a comment on this video with the top right button! + %s views + Thời gian: %s + Add an instance + Comments are not enabled on this video! + Pick up a resolution + Peertube favourites + The video has been added to bookmarks! + The video has been removed from bookmarks! + There is no Peertube videos in your favourites! + Kênh + Video + Kênh + Use Emoji One + Information + Display previews in all toots + New UX/UI designer + Display video previews + The account id has been copied in the clipboard! + Thay đổi ngôn ngữ + Ngôn ngữ mặc định + Truncate long toots + Truncate toots over \'x\' lines. Zero means disabled. + Display more + Display less + Manage tags + The tag already exists! + The tag has been stored! + The tag has been changed! + The tag has been removed! + Schedule boost + The boost is scheduled! + No scheduled boost to display! + Schedule boost.]]> + Art timeline + Open menu + Go back + Logo of the application + Profile picture + Profile banner + Contact admin of the instance + Add new + MastoHost logo + Emoji picker + Refresh + Expand the conversation + Remove an account + Remove the blocked domain + Custom emoji picker + Phát video + New toot + Image of the card + Hide media + Favicon + Add description for media (for the visually impaired) + + Không bao giờ + 30 phút + 1 giờ + 6 giờ + 12 giờ + 1 ngày + 1 tuần + + In this field, you need to write your instance host name.\nFor example, if you created your account on https://mastodon.social\nJust write mastodon.social (without https://)\n + You can start writing first letters and names will be suggested.\n\n + ⚠ The Login button will only work if the instance name is valid and the instance is up! + + More information + + Ngôn ngữ + Media only + Show NSFW + Crowdin translations + Crowdin manager + Translation of the application + About Crowdin + Bot + Pixelfed instance + Mastodon instance + Any of these + All of these + None of these + Any of these words (space-separated) + All these words (space-separated) + Add some words to filter (space-separated) + Change column name + Misskey instance + No app supporting this link is installed on your device. + Subscriptions + Overview + Trending + Recently added + Local + Upload + Reply + Delete a comment + Are you sure to delete this comment? + Full screen video + Mode for videos + Select the file to upload + My videos + Title + License + Category + Language + This video contains mature or explicit content + Enable video comments + Update video + Description + The video has been updated! + Upload cancelled! + The video has been uploaded! + Uploading, please wait… + Tap here to edit the video data. + Delete video + Are you sure to delete this video? + Display NSFW videos + No videos to display! + Leave a comment + Share + Choose a schedule mode + From device + From server + Toots (Server) + Toots (Device) + Modify + Display new toots above the \"Fetch more\" button + Timelines + Interface + Contacts + %1$s commented your video %2$s]]> + %1$s is following your channel %2$s]]> + %1$s is following your account]]> + %1$s has been published]]> + %1$s succeeded]]> + %1$s failed]]> + %1$s published a new video: %2$s]]> + %1$s has been blacklisted]]> + %1$s has been unblacklisted]]> + Export data + Import Data + Select the file to import + An error occurred when selecting the backup file! + Add a public comment + Send comment + There is no Internet connection. Your message has been stored in drafts. + Plain text + HTML + Markdown + Logout account + All + Support the app + Open Collective enables groups to quickly set up a collective, raise funds and manage them transparently. + Copy link + Connect + Normal + Compact + Console + Set display mode + Patch the Security Provider + Update tracking domains + The tracking data base has been updated! + http calls blocked by the application + List of blocked calls + Submit + The data base has been exported! + Featured hashtags + Filter timeline with tags + No tags + Hide the \"delete\" button in the notification tab + Attach an image when sharing a URL + + Poll + Polls + Create a poll + Choice 1 + Choice 2 + Choice %d + You need two choices at least for the poll! + Done + end at %s + Refresh poll + Vote + A poll you have voted in has ended + A poll you tooted has ended + Customize + Categories + Time slot + Advanced + Display \'new\' badge on unread toots + Peertube + Move timeline + Hide timeline + Reorder timelines + List permanently deleted + Followed instance removed + Pinned tag removed + Undo + You need to keep two visible tabs! + Reorder timelines + Main timelines can only be hidden! + BBCode + Always mark media as sensitive + GNU instance + Cached status + Forward tags in replies + Long press to store media + Blur sensitive media + Display timelines in a list + Display timelines + Mark bot accounts in toots + Manage tags + Remember the position in Home timeline + History + Playlists + Display name + You don\'t have any playlists. Tap on the \"+\" icon to add a new playlist + You must provide a display name! + The channel is required when the playlist is public. + Create a playlist + There is nothing in this playlist yet. + redo + Gallery + Emoji + Sticker + Eraser + Text + Filter + Brush + Are you sure you want to exit without saving the image? + Discard + Saving… + Image Saved Successfully! + Failed to save Image + Opacity + Enable photo editor + Add a poll item + Remove last poll item + Mute conversation + Unmute conversation + The conversation is no longer muted! + The conversation is muted + Open application features + Timed mute + Mention the account + Refresh cache + Mention the status + News + General + Regional + Art + Journalism + Activism + Gaming + Technology + Adult content + Furry + Food + Logo of the instance + Something went wrong when checking available instances! + Join Mastodon + Choose an instance by picking up a category, then tap on a check button. + Choose an instance by tapping on a check button. + %1$s users + Confirm password + I agree to %1$s and %2$s + server rules + terms of service + Sign up + This instance works with invitations. Your account will need to be manually approved by an administrator before being usable. + Please, fill all the fields! + Passwords don\'t match! + The email doesn\'t seem to be valid! + Your username will be unique on %1$s + You will be sent a confirmation e-mail + Use at least 8 characters + Password should contain at least 8 characters + Username should only contain letters, numbers and underscores + Account created! + Your account has been created!\n\n + Think to validate your email within the 48 next hours.\n\n + You can now connect your account by writing %1$s in the first field and click on Connect.\n\n + Important: If your instance required validation, you will receive an email once it is validated! + + Save the message in drafts? + Administration + Reports + No reports to display! + Reconnect the account + The application failed to access the admin features. You might need to reconnect the account for having the correct scope. + Unresolved + Remote + Active + Pending + Disabled + Silenced + Suspended + Permissions + Email status + Login status + Joined + Most recent IP + Warn + Disable + Silence + Notify the user per e-mail + Custom warning + User + Moderator + Administrator + Confirmed + Not confirmed + Reported statuses + Account + Undo silence + Undo disable + Suspend + Undo suspend + The account is silenced! + The account is no longer silenced! + The account is suspended! + The account is no longer suspended! + The account is disabled! + The account is no longer disabled! + The account has been warned! + Display the admin menu + Display the admin feature in statuses + Allow + The account is approved! + The account is rejected! + Assign to me + Unassign + Mark as resolved + Mark as unresolved + Empty content! + Display Fedilab features button + The application needs to access audio recording + Voice message + Enable quick reply + The account you are replying might not see your message! + If disabled, the app will always load last statuses + If disabled, sensitive media will be hidden with a button + Store media in full size with a long press on previews + Add an ellipse button at the top right for listing all tags/instances/lists + During the time slot, the app will send notifications. You can reverse (ie: silent) this time slot with the right spinner. + Display a Fedilab button below profile picture. It is a shortcut for accessing in-app features. + Allow to reply directly in timelines below statuses + Previews will not be cropped in timelines + Allow to play embedded videos directly in timelines + Allow to reverse the way to read statuses that are displayed once tapping the fetch more button + This option allows to support recent cipher suites. It is useful for older Android devices or if you cannot connect to your instance. + Exclusively for Peertube videos. Switch this mode if you cannot play them. + These tags will allow to filter statuses from profiles. You will have to use the context menu for seeing them. + Automatically insert a line break after the mention to capitalize the first letter + Allow content creators to share statuses to their RSS feeds + Compose + Maximum retry times when uploading media + Create a new Folder here + Enter the folder name + Please enter a valid folder name + This folder already exists.\n Please provide another name for the folder + Select + Default Directory + Folder + Create folder + Display a toast message after an action has been completed (boost, fav, etc.)? + Muted instances have been exported! + Add an instance + Export instances + Import instances + Crash reports + Enable crash reports + If enabled, a crash report will be created locally and then you will be able to share it. + Fedilab has stopped :( + You can send me by email the crash report. It will help to fix it :)\n\nYou can add additional content. Thank you! + Use the wysiwyg + When enabled, you will be able to format your text easily with tools. + Statistics + Total statuses + Number of boosts + Number of favourites + Number of mentions + Number of follows + Number of polls + Number of replies + Number of statuses + Statuses + Visibility + Number with media + Number with sensitive media + Number with CW + First status date + Last status date + First notification date + Last notification date + Frequency + %s statuses per day + %s notifications per day + Date range + Groups + No groups! + Disable custom animated emojis + Charts + Display charts + The application collects your local data, please wait… + Backup + Auto backup statuses + This option is per account. It will launch a service that will automatically store your statuses locally in the database. That allows to get statistics and charts + Auto backup notifications + This option is per account. It will launch a service that will automatically store your notifications locally in the database. That allows to get statistics and charts + Report account + Send an invitation + Your instance does not allow to register a new account! + + %d votes + + + %d voters + + + Single choice + Multiple choices + + + 5 minutes + 30 minutes + 1 hour + 6 hours + 1 day + 3 days + 7 days + + + Webview + Direct stream + + For joining my instance \"%1$s\", you can download Fedilab:\n\nF-Droid: %2$s\nGoogle: %3$s\n\nThen open the link below with Fedilab and create your account :)\n\n%4$s + + Your poll can\'t have duplicated options! + For all accounts + Database cache + Clear your home timeline cache + Clear your cached statuses + Clear your bookmarks + Files in cache + Total notifications + Hide menu items + Fedilab is running live notifications + For %1$s accounts with %2$s events + Live notifications for %1$s + Live notifications will be enabled for this account. + Clear cache when leaving + The cache (media, cached messages, data from the built-in browser) will be automatically cleared when leaving the application. + Do you want to unfollow this account? + Show confirmation dialog before unfollowing + Replace Youtube with Invidio.us + Invidious is an alternative front-end to YouTube + Enter your custom host or leave blank for using invidious.snopyta.org + Replace Twitter with Nitter + Nitter is an open source alternative Twitter front-end focused on privacy. + Enter your custom host or leave blank for using nitter.net + Replace Instagram with Bibliogram + Bibliogram is an open source alternative Instagram front-end focused on privacy. + Enter your custom host or leave blank for using bibliogram.art + Replace Reddit with Libreddit + Libreddit is an open source alternative Reddit front-end focused on privacy. + Enter your custom host or leave blank for using libredd.it + Replace Medium links + Replace medium.com links with an open source alternative front-end focused on privacy. + Default: scribe.rip + Replace Wikipedia links + Replace Wikipedia link with an open source alternative front-end focused on privacy. + Default: wikiless.org + Hide Fedilab notification bar + For hiding the remaining notification in the status bar, tap on the eye icon button then uncheck: \"Display in status bar\" + Use a push notifications system for getting notifications in real time. + No live notifications + Live notifications + Notifications will be fetched every 15 minutes. + Add notes + Notes for the account + Allow to compress large photos into smaller sized photos with very less or negligible loss in quality of the image. + Allow compressing videos while maintaining their quality. + The app is compressing the media, it can be quite long… + Change app icon + Tap to change the app icon + Post + Visibility of the post + Tap here to add photos + Accepted Formats: jpeg, png, gif \n\nMax File Size: 15 MB \n\nAlbums can contain up to 4 photos or videos + Upload media + Add an optional caption + The app received a very long error message from the API %1$s + Message preview + Add mentions in each message + Fetching conversation + Order by + Title for the video + Join Peertube + I am at least 16 years old and agree to the %1$s of this instance + Links + Change the color of links (URLs, mentions, tags, etc.) in messages + Reblogs header + Change the color of display name at the top of messages + Change the color of the user name at the top of messages + Change the color of the header for reblogs + Posts + Background color of posts in timelines + Reset colors + Tap here to reset all your custom colors + Reset + Icons + Color of bottom icons in timelines + Pin this tag + Logo of the instance + Edit profile + Make an action + Translation + Image preview + Text color + Change the text color in pots + Apply changes + You need to restart the application to apply changes + Restart + Use a custom theme + Allow to override colors of the selected theme above + Theming + Store before + The theme was exported + The theme has been successfully exported in CSV + Apply the primary color to the status bar + Status bar color + Restore a default theme + Import a theme + Tap here to import a theme from a previous export + Export the theme + Tap here to export the current theme + An error occurred when selecting the theme file + Theme Picker + Select a pre-installed theme + Themes + Apply the primary color to the navigation bar + Navigation bar color + The underlying color of the app’s content. + Background color + Accents select parts of the UI. + Accent color + Displayed most frequently across your app. + Primary color + Export bookmarks to the instance + Import bookmarks from the instance + User count + Status count + Instance count + Blocked + End in %s + What\'s new in %s + You can follow my account for updates + This instance is not available on https://instances.social + Display full link + Share link + The URL has been copied to the clipboard + Open with another app + Check redirect + This URL does not redirect + %1$s \n\nredirects to\n\n %2$s + Change the user agent + Set a custom user agent or leave blank + Allows to customize the user agent used for api calls or with the built-in browser. + Remove UTM parameters + The app will automatically remove UTM parameters from URLs before visiting a link. + Trends + Trending now + %d people talking + Twitter accounts (via Nitter) + Twitter usernames space separated + Identity proofs + Verified identity + Verified by %1$s (%2$s) + Delete the notification + Display more options + It is a Pixelfed story + Upload a media, it will be automatically added to your Pixelfed story. + Media successfully added to your story! + Action disabled + Unfollow + Something went wrong, please check your download directory in settings. + Announcements + No announcements! + Add a reaction + Use your favourite browser inside the app. Uncheck this feature to open links externally. + Video cache in MB, zero means no cache. + Watermarks + Automatically add a watermark at the bottom of pictures. The text can be customized for each account. + No distributors found! + You need a distributor for receiving push notifications.\nYou will find more details at %1$s.\n\nYou can also disable push notifications in settings for ignoring that message. + Select a distributor + diff --git a/app/src/main/res/values-w1240dp/dimens.xml b/app/src/main/res/values-w1240dp/dimens.xml new file mode 100644 index 000000000..d73f4a359 --- /dev/null +++ b/app/src/main/res/values-w1240dp/dimens.xml @@ -0,0 +1,3 @@ + + 200dp + \ No newline at end of file diff --git a/app/src/main/res/values-w600dp/dimens.xml b/app/src/main/res/values-w600dp/dimens.xml new file mode 100644 index 000000000..22d7f0043 --- /dev/null +++ b/app/src/main/res/values-w600dp/dimens.xml @@ -0,0 +1,3 @@ + + 48dp + \ No newline at end of file diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml new file mode 100644 index 000000000..bb60678dd --- /dev/null +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -0,0 +1,1134 @@ + + + 打开菜单 + 关闭菜单 + 关于 + 关于此实例 + 隐私 + 缓存 + 登出 + 登入 + + 关闭 + + + 取消 + 下载 + 下载 %1$s + 媒体已保存 + 文件:%1$s + 密码 + 电子邮箱 + 帐户 + 嘟文 + 标签 + 保存 + 恢复 + 没有结果! + 实例 + 实例:mastodon.social + 现在使用 %1$s 这个帐户 + 添加帐户 + 已复制嘟文的内容到剪贴板 + 已复制嘟文链接至剪贴板 + 更改 + 选择图片… + 清理 + 相机 + 全部删除 + 翻译这条嘟文。 + 定时 + 文本和图标大小 + 更改当前文本大小: + 更改当前图标大小: + 下一个 + 上一个 + 打开方式 + 确认 + 媒体 + 分享给 + 通过 Fedilab 分享 + 回复 + 用户名 + 草稿 + 收藏夹 + 新关注者 + 提及 + 转嘟 + 显示转嘟 + 显示回复 + 在浏览器中打开 + 翻译 + 请等待几秒钟,再进行此操作。 + + 主页 + 本地时间线 + 联合时间线 + 选项 + 收藏夹 + 交流 + 已静音用户 + 已屏蔽用户 + 通知 + 关注请求 + 设置 + 删除帐户 + 是否从应用程序中删除帐户 %1$s? + 发送电子邮件 + 点按路径以更改 + 失败! + 定时嘟文 + 以下信息可能不够完整反映该用户的资料。 + 插入表情符号 + 该应用程序当前没有收集自定义表情。 + 推送通知 + 确定要退出登录吗? + 确定要退出登录吗%1$s@%2$s? + + 没有嘟文可以显示 + 没有要显示的故事 + 故事 + 由 %1$s 转嘟 + 添加此嘟文到您的收藏夹中? + 从您的收藏夹中移除此嘟文? + 转嘟这条嘟文吗? + 取消转嘟这条嘟文吗? + 置顶此嘟文吗? + 取消此嘟文的置顶吗? + 静音 + 屏蔽 + 举报 + 移除 + 复制 + 分享 + 提及 + 定时静音 + 删除并加入草稿 + + 是否禁音此帐户? + 是否屏蔽此帐户? + 举报这条嘟文吗? + 是否屏蔽此域名? + 取消该用户的禁言吗? + 取消屏蔽此帐号? + + + 通知 + 静音 + + + 移除这条嘟文吗? + 删除并重新添加这条嘟文到草稿里? + + 书签 + 加入书签 + 移除书签 + 没有书签可以显示 + 状态已添加到书签! + 状态已从书签移除! + + %d 秒 + %d 分 + %d 小时 + %d 天 + + %d 秒 + + + %d 分钟 + + + %d 小时 + + + %d 天 + + + 警告 + 你在想什么? + 嘟嘟! + 啾鸣! + 内容警告 + 编写嘟文 + 回复嘟文 + 撰写啾文 + 回复啾文 + 选择媒体 + 选择媒体时出现了错误! + 删除此媒体吗? + 您的嘟文是空的! + 嘟文的可见性 + 默认情况下,嘟文的可见性: + 嘟文已发送! + 您正在回复此嘟文: + 敏感内容? + + 发布到公共时间线 + 不发布到公共时间线 + 仅发送给粉丝 + 仅发布给提及用户 + + 没有草稿! + 选择嘟文 + 选择一个帐号 + 选择一些帐号 + 移除草稿? + 点击按钮以显示原始嘟文 + 给视觉障碍人士的描述 + + 没有可用的描述! + + 版本 %1$s + 开发者: + 许可协议: + GNU GPL V3 + 源代码: + 嘟文的翻译: + 搜索实例: + 图标设计者: + + 对话 + + 没有帐户可以显示 + 无关注请求 + 嘟文\n%1$s + 关注\n%1$s + 粉丝\n%1$s + 置顶\n%d + 授权 + 拒绝 + + 没有定时嘟文可以显示! + 编写一条嘟文,然后从顶部菜单中选择 定时 + 删除该定时嘟文吗? + 媒体:%d + 嘟文将在预定时间发送! + 预定的时间必须比现在晚一小时! + 已启用省电模式!它可能无法按预期工作。 + + 静音时间应超过 1 分钟。 + %1$s 已被静音至 %2$s。\n您可以从他的档案页中取消静音。 + %1$s 已被静音至 %2$s。\n点击这里取消该帐号的静音。 + + 没有通知可以显示 + 提及了您 + 撰写一条新消息 + 转嘟了您的状态 + 收藏了您的状态 + 关注了您 + 请求关注你 + + 和另外 %d 条通知 + + + %d 赞 + + 删除一条通知吗? + 删除所有通知吗? + 已删除该通知! + 已删除所有通知! + + 关注 + 粉丝 + 置顶 + + 无法获取客户端 ID! + 无法连接到实例域! + 没有互联网连接! + 该帐号已被屏蔽! + 该帐号不再被屏蔽! + 该帐号已被静音! + 该帐号不再被静音! + 已关注此帐号! + 已取消关注此帐号! + 该嘟文已被转嘟! + 该嘟文已不再被转嘟! + 该嘟文已添加到您的收藏夹! + 该嘟文已从您的收藏夹中移除! + 该嘟文已被举报! + 该嘟文已被删除! + 该嘟文已被置顶! + 该嘟文已被取消置顶! + 哎呀!发生错误! + 发生错误!该实例未返回授权码! + 该实例域名似乎是无效的! + 在帐户间切换时发生错误! + 搜索时出错! + 个人资料数据已保存! + 无法进行任何操作 + 媒体已保存! + 翻译时出错! + 设置中已禁用翻译 + 草稿已保存! + 您确定此实例允许此数量的字符?通常,该值接近于 500 个字符。 + 已更改帐户 %1$s 的嘟文可见性 + + 每次加载的嘟文数量 + 始终 + WIFI + 询问 + 加载媒体 + 加载图片 + 显示更多… + 收起… + 敏感内容 + 禁用 GIF 头像 + 路径: + 自动保存草稿 + 在嘟文中添加媒体的 URL + 当有人关注您时通知 + 当某人转嘟您的状态时通知 + 当某人收藏您的状态时通知 + 当有人提到您时通知 + 当投票结束发出通知 + 通知新帖子 + 转嘟前显示确认对话框 + 添加到收藏夹前显示确认对话框 + 仅当使用 WiFi 时通知 + 开启通知? + 静音通知 + NSFW 查看超时(秒,0 表示关闭) + 媒体描述超时(秒,0表示关闭) + 编辑个人资料 + 自定义分享 + 您的自定义分享链接… + 简介… + 锁定帐户 + 保存修改 + 选择页眉图片 + 适应预览图像 + 自动在回复中拆分超过 500 字符的嘟文 + 您已达到允许的 160 字符上限! + 您已达到允许的 30 字符上限! + 介于 + + 时间必须大于 %1$s + 时间必须小于 %1$s + 起始时间 + 结束时间 + 使用内置浏览器 + 自定义标签页 + 启用 Javascript + 自动展开带有警告的内容 + 允许第三方 cookie + 您的 API 密钥,对于 Yandex 您可以留空 + + 暗色 + 亮色 + 黑色 + + 设置 LED 颜色: + + 蓝色 + 青色 + 洋红色 + 绿色 + 红色 + 黄色 + 白色 + + 关注 + 取消屏蔽 + 静音 + 取消静音 + 请求已发送 + 关注您 + 搜索 + 回复时首字母大写 + 调整图片大小 + 调整视频大小 + + 推送通知 + 请确认要接收的推送通知。 + 您可以稍后在设置(通知选项卡)中启用或禁用这些通知。 + + + 清除缓存 + 缓存中有 %1$s 的数据。\n\n您想要删除它们吗? + Mb + 缓存已清除!已释放 %1$s + + 标题 + 标题… + 描述 + 关键词 + 关键词… + + 同步 + 筛选 + 您的嘟文 + 您的通知 + 公开 + 未列出 + 私密 + 私信 + 一些关键词… + 显示媒体 + 显示置顶 + 未找到匹配的结果! + 备份 %1$s 的嘟文 + 已导入 %1$s 的新嘟文 + %1$s 新的通知已导入 + + 按日期降序排序 + 按日期升序 + + + + + 二者 + + 在数据库中未发现嘟文,请使用菜单中的同步按钮来检索它们。 + + 记录的数据 + 只有帐户中的基本信息才会存储在设备上。 + 这些数据是严格保密的,而且只能由应用程序使用。 + 删除应用程序将立即删除这些数据。\n + ⚠ 从不存储登录和密码。它们仅在与实例进行安全身份验证(SSL)时使用。 + + 权限: + - ACCESS_NETWORK_STATE:用于检测设备是否连接到 WIFI 网络。\n + - INTERNET:用于查询实例。\n + - WRITE_EXTERNAL_STORAGE:用于存储媒体或将应用程序移动到 SD 卡上。\n + - READ_EXTERNAL_STORAGE:用于添加媒体到嘟文。\n + - BOOT_COMPLETED:用于启动通知服务。\n + - WAKE_LOCK:在通知服务期间使用。 + + API 权限: + - Read:读取数据。\n + - Write:发布状态并上传用在状态上的媒体。 \n + - Follow:关注、取消关注、屏蔽、取消屏蔽。\n\n + ⚠ 这些操作仅在用户请求时执行。 + + 跟踪和库 + 本应用程序 不使用跟踪工具(受众测量、错误报告等),并且不包含任何广告。\n\n + 所使用的库也是最为精简的:\n + -Glide:用于管理媒体\n + -Android-Job:用于管理服务\n + -PhotoView:用于管理图像\n + + 嘟文的翻译 + 本应用程序提供了使用设备的区域设置和 Yandex API 进行翻译的功能。\n + Yandex 有适当的隐私政策,可以在这里找到:https://yandex.ru/legal/confidential/?lang=en + + 致谢: + + 按正则表达式进行筛选 + 搜索​​​​ + 删除 + 获取更多嘟文… + + 列表 + 您确实要永久删除此列表吗? + 这张列表上还没有内容。当此列表的成员发布新状态时,它们将出现在此处。 + 添加到列表 + 新建列表 + 删除列表 + 编辑列表 + 新列表标题 + 帐号已添加到列表! + 您暂时没有任何列表! + + %1$s 已被移动到 %2$s + 身份验证不起作用? + 以下是一些可能有用的检查:\n\n + - 检查实例名是否拼写错误\n\n + - 检查您的实例是否下线\n\n + - 如果您使用双因素身份验证(2FA),请使用底部的链接(实例名填好后)\n\n + - 您也可以使用这个链接但不使用双因素身份验证(2FA)\n\n + - 如果仍不工作,请向 https://framagit.org/tom79/fedilab/issues 提交问题。 + + 媒体已加载。单击此处显示。 + 此操作可能会很久。完成时会通知您。 + 仍在运行,请稍候…… + 导出状态 + 导出 %1$s 的状态 + %2$s 条嘟文中的 %1$s 条已被导出。 + 导出 %1$s 的数据时出现错误 + 导出数据时出现错误! + 导入数据时发生错误! + + 代理 + 启用代理? + 主机 + 端口 + 用户名 + 密码 + 分享时添加嘟文的详细信息 + 在 Liberapay 上支持这个应用 + 正则表达式中有错误! + 未在此实例中找到时间线! + 删除此实例? + 翻译为 + 关注实例 + 您已关注过该实例! + 已关注此实例! + 合作伙伴 + 信息 + 隐藏 %s 的转嘟 + 推荐在个人页上 + 显示 %s 的转嘟 + 取消推荐在个人页上 + 该帐号现在已推荐到个人页上 + 该帐号已不再推荐到个人页上 + 现已显示转嘟! + 现已隐藏转嘟! + 私信 + 筛选器 + 没有筛选器可以显示。您可以点击“+”按钮新建一个。 + 关键词或短语 + 主时间线 + 公共时间线 + 通知 + 对话 + 匹配将忽略嘟文的文本大小写或内容警告 + 丢弃而不是隐藏 + 筛选的嘟文将永久消失,即使后来移除筛选器 + 如果关键词或短语只为字母数字时,将只在词语完全匹配时才会应用 + 完全匹配词语 + 筛选内容 + 筛选器将应用到的一个或多个内容 + 过期于 + 删除筛选器? + 更新筛选器 + 创建筛选器 + 关注推荐 + 目前还没有可列出的帐号! + 关注 + 选择全部 + 取消全选 + 已关注 %s! + 创建列表 %s + 添加帐号到列表 + 帐号已添加到列表 + 正在添加帐号到列表 + 您还未创建列表。点击“+”按钮新建一个。 + 关注推荐 + Trunk API + 无法关注帐号 + 获取远程帐号 + 自动展开隐藏媒体 + 新的粉丝 + 新的转嘟 + 新的收藏 + 新的提及 + 投票已结束 + 新的嘟文 + 嘟文备份 + 新帖子 + 媒体下载 + 更改通知提示音 + 选择铃声 + 启用时间段 + 操作方法视频 + 正在获取远程线程! + 没有屏蔽的域名! + 取消屏蔽域名 + 您确定要取消屏蔽 %s 吗? + 您确定要屏蔽 %s 吗? + 已屏蔽域名 + 屏蔽域名 + 该域名已被屏蔽 + 该域名不再被屏蔽 + 正在获取远程状态 + 评论 + Peertube 实例 + 使用右上角的按钮,成为第一个对此视频留言的人! + %s 次观看 + 时长:%s + 添加实例 + 此视频未开启评论! + 选择分辨率 + Peertube 收藏夹 + 此视频已添加到书签! + 此视频已从书签中移除! + 您的收藏夹里没有 Peertube 视频。 + 频道 + 视频 + 频道 + 使用 Emoji One + 信息 + 显示所有嘟文中的预览 + 新 UX/UI 的设计师 + 显示视频预览 + 帐号 ID 已复制到剪贴板! + 更改语言 + 默认语言 + 截断长嘟文 + 截断超过“x”行的嘟文。零代表禁用。 + 显示更多 + 收起 + 管理标签 + 该标签已经存在! + 该标签已储存! + 该标签已更改! + 该标签已删除! + 定时转嘟 + 转嘟将在预定时间发布! + 没有预定的转嘟可以显示! + 定时转嘟。]]> + 艺术时间线 + 打开菜单 + 返回 + 应用程序的徽标 + 个人资料图片 + 个人资料横幅 + 联系该实例的管理员 + 新添 + MastoHost 徽标 + 表情符号选择器 + 刷新 + 展开对话 + 移除帐户 + 删除屏蔽的域名 + 自定义表情符号选择器 + 播放视频 + 新嘟文 + 卡片图像 + 隐藏媒体 + 网站图标 + 要添加描述的媒体 + + 从不 + 30 分钟 + 1 小时 + 6 小时 + 12 小时 + 1 天 + 1 星期 + + 您需要在此字段中填入您的实例主机名。\n例如,您在 https://mastodon.social\n 上创建了帐户,直接填入 mastodon.social(不需要 https://)\n + 您开始填入首个字母后会提示主机名。\n\n + ⚠ 登入按钮只有在实例名有效且该实例已上线时有效! + + 更多信息 + + 语言 + 仅限媒体 + 显示 NSFW + Crowdin 翻译 + Crowdin 管理员 + 应用程序的翻译 + 关于 Crowdin + 机器人 + Pixelfed 实例 + 长毛象实例 + 任一 + 全部 + 除开 + 任一关键词(以空格隔开) + 全部关键词(以空格隔开) + 新增一些文字到过滤器(以空格分开) + 更改列名称 + Misskey 实例 + 您的设备上没有安装任何支持此链接的应用。 + 订阅 + 概览 + 趋势 + 最近添加 + 本地 + 上传 + 回复 + 删除评论 + 你确定要删除此评论吗? + 全屏视频 + 视频模式 + 选择要上传的文件 + 我的视频 + 标题 + 许可协议 + 类别 + 语言 + 此视频包含成人或露骨的内容 + 启用视频评论 + 更新视频 + 描述 + 视频已更新! + 已取消上传! + 视频已上传! + 正在上传,请稍候…… + 点击此处编辑视频数据。 + 删除视频 + 你确定要删除此视频吗? + 显示 NSFW 视频 + 没要视频可以显示! + 发表评论 + 分享 + 选择计划模式 + 来自设备 + 来自服务器 + 嘟文(服务器) + 嘟文(设备) + 修改 + 在“获取更多”按钮上方显示新嘟文 + 时间线 + 界面 + 联系人 + %1$s 评论了你的视频 %2$s]]> + %1$s 关注了您的频道 %2$s]]> + %1$s 关注了您的账户]]> + %1$s 已发布]]> + %1$s]]> + %1$s 失败]]> + %1$s 发布了新视频:%2$s]]> + %1$s 已被加入黑名单]]> + %1$s 已被取消黑名单]]> + 导出数据 + 导入数据 + 选择要导入的文件 + 选择备份文件时发生错误! + 添加公共评论 + 发送评论 + 无互联网连接。您的消息已保存在草稿里。 + 纯文本 + HTML + Markdown + 登出帐号 + 全部 + 支持应用 + 开放群体能使群组快速构建集体募集资金并透明化管理。 + 复制链接 + 连接 + 正常视图 + 精简视图 + 控制台视图 + 设置显示模式 + 修补安全供应商 + 更新跟踪域名 + 已更新跟踪数据库! + 应用程序阻止了 HTTP 通话 + 阻挡通话列表 + 提交 + 已导出数据库! + 特色标签 + 根据标签过滤时间线 + 无标签 + 隐藏通知栏的“删除”按钮 + 分享 URL 时附加图像 + + 投票 + 投票 + 创建投票 + 选项一 + 选项二 + 选择 %d + 你至少需要两个选项才能开投票! + 完成 + 在 %s 结束 + 刷新投票 + 投票 + 一个你参加过的投票已经结束 + 您所嘟出的一个投票已经结束 + 定制 + 类别 + 时隙 + 高级 + 在未读嘟文上显示“新”徽章 + Peertube + 移动时间线 + 隐藏时间线 + 记录时间线 + 列表永久性地被删除 + 所关注的实例已被移除 + 钉住标签已被移除 + 撤销 + 您需要保持两条可见的时间线! + 记录时间线 + 主时间线只能被隐藏! + BBCode + 总是标记媒体为敏感 + GNU实例 + 高速缓冲的状态 + 在回复中转发标签 + 长按来存储媒体 + 模糊敏感媒体 + 在一个列表中显示时间线 + 显示时间线 + 在嘟文中标记机器人账户 + 管理标签 + 记住在主时间线中的位置 + 历史 + 播放列表 + 显示名称 + 你没有任何播放列表。 点击\"+\"图标添加一个新的播放列表 + 你必须提供显示的名字! + 当播放列表是公开的时,该频道是需要的。 + 创建播放列表 + 在这个播放列表中还没有任何内容。 + 重做 + 相册 + Emoji 表情 + 贴纸 + 橡皮擦 + 文本 + 过滤器 + 画笔 + 您确定要退出而不保存图像吗? + 舍弃 + 正在保存… + 图像保存成功! + 保存图像失败 + 不透明度 + 启用照片编辑器 + 添加一个投票项目 + 删除上一个投票项 + 静音对话 + 取消静音对话 + 对话不再被静音! + 对话是静音的 + 打开应用程序功能 + 定时禁言 + 提及帐户 + 刷新缓存 + 提及状态 + 新闻 + 常规​​​​​ + 区域 + 艺术 + 期刊 + 活动 + 游戏 + 技术 + 成人内容 + 兽迷 + 美食 + 实例的标志 + 检查可用实例时出错! + 加入乳齿象 + 通过挑选一个类别选择一个实例,然后点击检查按钮。 + 点击检查按钮来选择一个实例。 + %1$s 个用户 + 确认密码 + 我同意 %1$s 和 %2$s + 服务器规则 + 服务条款 + 注册 + 此实例为邀请有效。您的帐户需要先由管理员手动批准才能使用。 + 请填写所有字段! + 密码不匹配! + 电子邮件似乎无效! + 您的用户名将在 %1$s 上是唯一的 + 您将收到确认电子邮件 + 至少使用8个字符 + 密码至少应包含8个字符 + 用户名仅应包含字母、数字和下划线 + 帐户已创建! + Your account has been created!\n\n + Think to validate your email within the 48 next hours.\n\n + You can now connect your account by writing %1$s in the first field and click on Connect.\n\n + Important: If your instance required validation, you will receive an email once it is validated! + + 将此信息保存为草稿? + 管理 + 报告 + 没有要显示的报告! + 重新连接账户 + 应用程序无法访问管理员功能。您可能需要重新连接账户以拥有正确的范围。 + 未解决 + 远程 + 活动中 + 待处理 + 已禁用 + 静音 + 暂停 + 权限 + 电子邮件状态 + 登录状态 + 加入 + 最近的 IP + 警告 + 禁用 + 静音 + 每个电子邮件都通知用户 + 自定义警告 + 用户 + 主持人 + 管理员 + 确认 + 未确认 + 被报告的状态 + 帐户 + 撤销禁音 + 撤销禁用 + 暂停 + 撤销暂停 + 账户被静音! + 账户不再被静音! + 账户被暂停! + 账户不再被暂停! + 账户已禁用! + 账户不再禁用! + 账户已被警告! + 显示管理菜单 + 在状态中显示管理员功能 + 允许 + 账户已被批准! + 账户被拒绝! + 分配给我 + 取消分配 + 标记为已解决 + 标记为未解决 + 空内容! + 显示 Fedilab 功能按钮 + 应用程序需要访问音频录制 + 语音消息 + 启用快速回复 + 您正在回复的帐户可能看不到您的信息! + 如禁止,总是装载最后状态。 + 如禁止,敏感媒体总被用一个按钮隐藏 + 在预览视图上长按可以全尺寸存储媒体 + 在右上角添加一个省略按钮来列出所有标签/实例/列表 + 在时间段期间,应用程序将发送通知。您可以用正确的选择逆反 (如: 静音) 这个时间段。 + 在档案图片下显示 Fedilab 按钮。它是访问应用内功能的快捷方式。 + 允许在时间线状态下直接回复 + 预览不会在时间线中裁剪 + 允许直接在时间线中播放嵌入式视频 + 允许扭转在点击获取更多按钮时显示的读取状态的方式 + 此选项允许支持最近的加密套件。对于旧 Android 设备,或者如果您不能连接到您的实例,这是有用的。 + 仅用于 Peertomultue 视频。如果您不能播放,请切换此模式。 + 这些标签将允许从配置文件中筛选状态。您需要使用上下文菜单来查看这些状态。 + 在提及后自动插入一个行间来大写第一个字符 + 允许内容创建者共享 状态到他们的RSS 订阅 + 撰写 + 上传图片最大重试次数 + 在此创建新文件夹 + 输入文件夹名 + 请输入一个有效的文件夹名 + 文件夹已存在。\n +请提供另一个文件夹名 + 选择 + 默认目录 + 文件夹 + 创建文件夹 + 操作完成后显示一个 toast 消息(转嘟、收藏等)? + 已导出静音实例! + 添加实例 + 导出实例 + 导入实例 + 崩溃报告 + 启用崩溃报告 + 如果启用,崩溃报告将在本地创建,然后您可以分享它。 + Fedilab 已停止 :( + 您可以通过电子邮件给我发送崩溃报告。这有助于修复问题 :)\n\n您可以添加其他内容。谢谢! + 使用所见即所得 + 启用后,您可以轻松格式化您的文本和工具。 + 统计 + 总状态 + 转嘟数 + 收藏数量 + 提及的数量 + 关注数量 + 投票次数 + 答复数量 + 状态数量 + 状态 + 可见性 + 带媒体的数量 + 带敏感媒体的数量 + 带有 CW 的数量 + 第一状态日期 + 最后状态的日期 + 第一个通知日期 + 最后通知日期 + 频率 + %s 状态/天 + %s 通知/天 + 日期范围 + + 没有组! + 禁用自定义动画表情 + 图表 + 显示图表 + 应用程序正在收集您的本地数据,请稍候…… + 备份 + 自动备份状态 + 此选项是每个帐号单独设置的。它会启动服务,将你的状态自动储存到本地数据库里。这样可以获取统计数据和图表 + 自动备份通知 + 此选项是每个帐号单独设置的。它会启动服务,将你的通知自动储存到本地数据库里。这样可以获取统计数据和图表 + 举报帐号 + 发送邀请 + 你的实例不允许注册新帐户! + + %d 票 + + + %d 位投票者 + + + 单选 + 多选 + + + 5 分钟 + 30 分钟 + 1 小时 + 6 小时 + 1 天 + 3 天 + 7 天 + + + Webview + 直接串流 + + 要加入我的实例“%1$s”,你可以下载 Fedilab:\n\nF-Droid:%2$s\nGoogle:%3$s\n\n然后打开下面这个带有 Fedilab 的链接就可以创建你的帐户了 :)\n\n%4$s + 你的投票不能有重复的选项! + 对所有帐号执行 + 数据库缓存 + 清除你的主页时间线缓存 + 清除你缓存的状态 + 清除你的书签 + 缓存文件 + 通知总数 + 隐藏菜单项目 + Fedilab 正运行实时通知 + %1$s 个帐号,总共 %2$s 条活动 + %1$s 的实时通知 + 将启用此帐号的实时通知。 + 离开时清除缓存 + 离开此应用程序时,将自动清除缓存(媒体、缓存消息、内建浏览器的数据)。 + 你确定要取消关注此帐号吗? + 取消关注前显示确认对话框 + 用 Invidio.us 替换 Youtube + Invidious 是个 YouTube 的替代前端 + 输入你自定义的主机或者留空以使用 invidio.us + 用Nitter取代Twitter + Niter是一种开放源码的替代的 Twitter 的前端,注重保护隐私。 + 输入你自定义的主机或者留空以使用 nitter.net + 将 Instagram 替换成 Bibliogram + Bibliogram 是个注重隐私保护的 Instagram 开源替代前端。 + 输入你的自定义主机或者留空以使用 bibliogram.art + 用 Libreddit 替换 Reddit + Libreddit 是一个开源的 Reddit 替代前端,专注于隐私。 + 输入您的自定义主机或留空使用 libredd.it + 替换 Medium 链接 + 用专注于隐私的开源替代前端替换 medium.com 链接。 + 默认: scribe.rip + 替换 Wikipedia 链接 + 用专注于隐私的开源替代前端替换维基百科链接。 + 默认: wikiless.org + 隐藏 Fedilab 通知栏 + 要隐藏留在状态栏里的通知,请点击眼睛图标的按钮,然后取消勾选:“在状态栏内显示” + 使用推送通知系统实时获取通知。 + 没有直播通知 + 实时通知 + 通知将被延迟为每15分钟产生。 + 添加注释 + 账户说明 + 允许将大型照片压缩到较小大小的照片,图像质量丢失甚少或微不足道。 + 允许在保持视频质量的同时压缩视频。 + 该应用正在压缩媒体,它可能需要相当长时间… + 更改应用图标 + 点击更改应用图标 + 发布 + 帖子的可见性 + 点击此处添加照片 + 接受的格式:jpeg、png、gif \n\n最大文件大小:15 MB \n\n相册可以包含最多 4 张照片或视频 + 上传媒体 + 添加一个可选标题 + 应用程序收到了 API 的 %1$s 非常长的错误消息 + 消息预览 + 在每条消息中添加提及 + 正在获取对话 + 排序方式 + 视频标题 + 加入Peertube + 我至少年满16岁,并同意此实例的 %1$s + 链接 + 更改消息中链接的颜色 (URL、提及、标签等) + 重发博客标题 + 更改消息顶部显示名称的颜色 + 更改消息顶部用户名的颜色 + 更改重发博客头的颜色 + 发布 + 时间线中发布的背景颜色 + 重置颜色 + 点击此处重置所有自定义颜色 + 重置 + 图标 + 时间线底部图标颜色 + 固定此标签 + 实例的标志 + 编辑个人资料 + 添加操作 + 翻译 + 图像预览 + 文本颜色 + 更改点的文本颜色 + 应用更改 + 您需要重新启动应用程序才能应用更改 + 重新启动 + 使用自定义主题 + 允许覆盖上面所选主题的颜色 + 主题 + 存放之前 + 主题已导出 + 主题已成功地以CSV格式导出 + 将首要颜色应用到状态栏 + 状态颜色 + 恢复默认主题 + 导入主题 + 点击此处从先前导出的主题导入主题 + 导出主题 + 点击此处以导出当前主题 + 选择主题文件时出错 + 主题选择器 + 选择预安装的主题 + 主题 + 导航栏背景颜色将与界面的主色调相同 + 导航栏颜色 + 应用程序内容的底色。 + 背景颜色 + UI 主题色选择部分。 + 主题色 + 在应用程序中最常用的颜色。 + 主要颜色 + 导出书签到实例 + 从实例导入书签 + 用户数 + 状态数 + 实例数 + 已阻止 + 结束于 %s + %s 的新功能 + 您可以关注我的帐户以获取更新 + 此实例在 https://instance.social 上不可用 + 显示完整链接 + 分享链接 + 网址已复制到剪贴板 + 使用另一个应用打开 + 检查重定向 + 此网址没有重定向 + %1$s \n\n重定向到\n\n %2$s + 更改用户代理 + 设定自定义代理或留空 + 允许自定义用于 API 调用或内建浏览器的用户代理。 + 移除 UTM 参数 + 在访问链接之前,应用将自动从网址中删除 UTM 参数。 + 趋势 + 热门 + %d 人正讨论 + Twitter 帐号(通过 Nitter) + Twitter 用户名以空格隔开 + 身份证明 + 已验证身份 + 验证通过 %1$s (%2$s) + 删除该通知 + 显示更多选项 + 这是Pixelfed的故事 + 上传媒体,它将自动添加到您的 Pixelfed 故事中。 + 媒体成功添加到您的故事中! + 动作已禁用 + 撤销关注 + 出错了,请在设置中检查你的下载目录。 + 通告 + 无通告! + 添加回应 + 在应用内使用您最喜欢的浏览器。取消勾选此功能以对外打开链接。 + 以 MB 计算的视频缓存量,零表示无缓存。 + 水印: + 自动在图片底部添加一个水印。可以为每个帐户定制文本。 + 没有找到分发商! + 要接收推送通知你需要一个分发商。\n你将在%1$s找到更多详情。\n\n你也可以在设置中禁用推送通知,以忽略该消息。 + 选择一个分发商 + diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml new file mode 100644 index 000000000..c7474ca4f --- /dev/null +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -0,0 +1,1133 @@ + + + 開啟選單 + 關閉選單 + 關於 + 關於站台 + 隱私權 + 快取 + 登出 + 登入 + + 關閉 + + + 取消 + 下載 + 下載 %1$s + 媒體已儲存 + 檔案:%1$s + 密碼 + 電子郵件 + 帳號 + 嘟文 + 標籤 + 儲存 + 還原 + 沒有結果! + 站台 + 站台:mastodon.social + 現在使用帳號 %1$s + 新增帳號 + 已將嘟文的內容複製到剪貼簿 + 已將嘟文網址複製到剪貼簿中 + 變更 + 選取圖片… + 清理 + 相機 + 全部刪除 + 翻譯此嘟文。 + 排程 + 文字與圖示大小 + 更改目前文字大小: + 變更目前圖示大小: + 下一個 + 上一個 + 選擇開啟工具 + 確認 + 媒體 + 分享給 + 已透過 Fedilab 分享 + 回覆 + 使用者名稱 + 草稿 + 最愛 + 新的關注者 + 提及 + 轉嘟 + 顯示轉嘟 + 顯示回覆 + 在瀏覽器中開啟 + 翻譯 + 請等待幾秒鐘再進行此操作。 + + 首頁 + 本地時間軸 + 站點聯盟時間軸 + 選項 + 最愛 + 通訊 + 已靜音的使用者 + 被封鎖的使用者 + 通知 + 關注請求 + 設定 + 刪除帳號 + 是否從應用程式刪除 %1$s 帳號? + 傳送電子郵件 + 點選路徑變更 + 失敗! + 排程的嘟文 + 下面的資訊可能不完全反映使用者的個人資料。 + 插入表情符號 + 應用程式目前不能收集自訂表情符號。 + 推播通知 + 您確定您要登出嗎? + 您確定您要登出 @%1$s@%2$s 嗎? + + 尚無嘟文 + 沒有可顯示的動態 + 動態 + 被 %1$s 轉嘟 + 將此嘟文新增到您的最愛? + 從您的最愛中移除此嘟文? + 轉嘟此嘟文? + 移除此嘟文的轉嘟? + 是否釘選此嘟文? + 是否取消此嘟文的釘選? + 靜音 + 封鎖 + 檢舉 + 移除 + 複製 + 分享 + 提及 + 定時靜音 + 刪除 & 變回草稿 + + 靜音此帳號? + 封鎖此帳號? + 檢舉此嘟文? + 是否封鎖該站點? + 取消靜音此帳號? + 解除封鎖此帳號? + + + 通知 + 靜音 + + + 移除此嘟文? + 刪除 & 並將此嘟文變回草稿? + + 書籤 + 新增至書籤 + 移除書籤 + 尚無書籤 + 已將此嘟文加入書籤! + 已將此嘟文從書籤中移除! + + %d 秒 + %d 分鐘 + %d 小時 + %d 天 + + %d秒 + + + %d分 + + + %d小時 + + + %d天 + + + 警告 + 您正在想些什麼? + 嘟出去! + QUEET! + cw + 撰寫一則嘟文 + 回覆一則嘟文 + 寫一篇 queet + 回覆一則 queet + 選取媒體 + 選擇媒體時發生錯誤! + 刪除此媒體? + 您的嘟文是空的! + 嘟文可見度 + 預設情況下的嘟文可見度: + 已送出嘟文! + 您正在回覆此嘟文: + 敏感內容? + + 張貼到公開時間軸 + 不要張貼到公開時間軸 + 僅關注者可見 + 僅提及的使用者可見 + + 沒有草稿! + 選擇一個嘟文 + 選擇一個帳號 + 選取一些帳號 + 移除草稿? + 點選按鈕以顯示原始嘟文 + 為視力障礙者提供描述 + + 沒有可用的描述! + + 釋出 %1$s + 開發者: + 授權條款: + GNU GPL V3 + 原始碼: + 嘟文翻譯提供者: + 搜尋站台: + 圖示設計者: + + 對話 + + 沒有帳號 + 無關注請求 + 嘟文\n%1$s + 正在關注\n %1$s + 關注者\n %1$s + 已釘選\n %d + 授權 + 拒絕 + + 沒有已排程的嘟文! + 先撰寫嘟文,再從頂端選單選擇排程 + 刪除排程的嘟文? + 媒體:%d + 嘟文已經排程! + 嘟文排程的時間必須大於目前的時間! + 已開啟電量節省模式!它可能無法如預期般運作。 + + 靜音時間應該大於一分鐘。 + %1$s 已經靜音到 %2$s。\n 您可以從他/她的個人資料頁面解除靜音該帳號。 + %1$s 被靜音到 %2$s。\n 點選這裡以取消靜音此帳號。 + + 沒有通知 + 提及您 + 撰寫新郵件 + 已轉嘟您的嘟文 + 將您的嘟文加到最愛 + 關注了您 + 要求追蹤您 + + 和另外 %d 個通知 + + + %d 個喜歡 + + 刪除通知? + 刪除全部通知? + 通知已被刪除! + 所有通知都已刪除! + + 正在關注 + 關注者 + 已釘選 + + 無法取得客戶端 id! + 無法連線到站台所在的網域! + 沒有網路連線! + 帳號被封鎖! + 帳號不再被封鎖! + 帳號被靜音! + 帳號不再靜音! + 帳號已被關注! + 帳號不再被關注! + 嘟文被轉嘟了! + 嘟文無法再被轉嘟了! + 嘟文已新增至您的最愛! + 嘟文已從您的最愛中移除! + 嘟文已被回報! + 嘟文已被刪除! + 嘟文已被釘選! + 嘟文已被取消釘選! + 唉呀!發生錯誤! + 發生錯誤!站台未回傳授權代碼! + 站台網域似乎不是有效的網域! + 在帳號間切換時發生錯誤! + 搜尋時發生錯誤! + 個人資料已儲存! + 無法採取行動 + 媒體已儲存! + 翻譯時發生錯誤! + 已在設定中停用翻譯 + 草稿已儲存! + 您確定此站台允許此數量的字元嗎?一般來說,此值約為 500 個字元左右。 + %1$s 帳號的嘟文可見度已變更 + + 每次載入的嘟文數量 + 總是 + Wi-Fi + 詢問 + 載入媒體 + 載入圖片 + 顯示更多…… + 顯示較少…… + 敏感內容 + 停用 GIF 大頭貼 + 路徑: + 自動儲存草稿 + 在嘟文中新增媒體的 URL + 當有人關注您時通知您 + 當有人轉嘟您的嘟文時通知 + 當有人收藏您的嘟文時通知您 + 當有人提及您時通知 + 投票結束時通知 + 通知新貼文 + 在轉嘟前顯示確認對話框 + 在新增至最愛前顯示確認對話框 + 僅在使用 Wi-Fi 時才啟用通知 + 通知? + 靜音通知 + NSFW 檢視逾時(秒,0 代表關閉) + 媒體描述逾時(秒,0 代表關閉) + 編輯個人資料 + 自訂分享 + 您的自訂分享 URL… + 簡介…… + 鎖定帳號 + 儲存變更 + 選擇頁首圖片 + 適當的預覽圖片 + 自動分離超過 500 個字元的嘟文到回覆中 + 您已經到達允許的 160 個字元! + 您已經到達允許的 30 個字元! + 介於 + 以及 + 時間必須大於 %1$s + 時間必須低於 %1$s + 開始時間 + 結束時間 + 使用內建瀏覽器 + 內嵌分頁 + 啟用 Javascript + 自動展開 cw + 允許第三方 cookies + 您的 API 金鑰,若為 Yandex 則可留空 + + + + + + 設定 LED 顏色: + + 藍色 + 青色 + 洋紅色 + 綠色 + 紅色 + 黃色 + 白色 + + 關注 + 解除封鎖 + 靜音 + 取消靜音 + 已送出請求 + 關注您 + 搜尋 + 回覆的第一個字母大寫 + 調整圖片大小 + 調整影片大小 + + 推播通知 + 請確認要接收的推播通知。 + 您稍後可以在設定(通知分頁)中啟用或停用這些通知。 + + + 清除快取 + 快取中有 %1$s 的資料。\n\n 您要刪除它們嗎? + Mb + 快取已清除!%1$s 被釋放 + + 標題 + 標題… + 描述 + 關鍵字 + 關鍵字… + + 同步 + 過濾 + 您的嘟文 + 您的通知 + 公開 + 不列出 + 私人 + 直接 + 一些關鍵字…… + 顯示媒體 + 顯示已釘選的 + 找不到符合的結果! + %1$s 的嘟文備份 + %1$s 的新嘟文已匯入 + 已匯入 %1$s 個新通知 + + 日期遞減 + 日期遞增 + + + + + 兩者 + + 在資料庫中找不到嘟文。請從選單中使用同步按鈕以擷取它們。 + + 已記錄的資料 + 只有帳號中的基本資訊才會儲存在裝置上。\n +這些資料嚴格保密,僅能由應用程式使用。\n +刪除應用程式將會立即移除這些資料。\n +⚠ 不會儲存密碼與登入資訊。它們僅會在有安全驗證 (SSL) 的站台上使用。 + + 權限: + - ACCESS_NETWORK_STATE:用於偵測裝置是否連線至 WIFI 網路。\n +- INTERNET:用於查詢站台。\n +- WRITE_EXTERNAL_STORAGE:用於儲存媒體或將應用程式移動到記憶卡上。\n +- READ_EXTERNAL_STORAGE:用於將媒體新增到嘟文中。\n +- BOOT_COMPLETED:用於啟動通知服務。\n +- WAKE_LOCK:在通知服務期間使用。 + + API 權限: + - Read:讀取資料。\n + - Write:貼出狀態與為狀態上傳媒體。\n + - Follow:關注、取消關注、阻擋、取消阻擋。\n\n + ⚠ 這些動作僅在使用者要求執行時才會執行。 + + 追蹤與函式庫 + 本應用程式不使用追蹤工具(粉絲追蹤、錯誤回報等等)且不包含任何廣告。\n\n + 使用的函式庫也盡量減到最少:\n + - Glide:管理媒體\n + - Android-Job:管理服務\n + - PhotoView:管理圖片\n + + 嘟文翻譯 + 該應用程式提供了使用裝置的語系設定與 Yandex API 進行翻譯的功能。\n +Yandex 有適當的隱私權政策,可以在這裡找到:https://yandex.ru/legal/confidential/?lang=en + + 感謝您: + + 按正規表達式進行過濾 + 搜尋 + 刪除 + 擷取更多嘟文…… + + 列表 + 您確定您想要永久刪除此列表嗎? + 這份列表裡沒有東西。當此列表的成員張貼新的狀態時,它們將會出現在這裡。 + 新增到列表 + 新增列表 + 刪除列表 + 編輯列表 + 新列表標題 + 已將帳號新增至清單中! + 您還沒有任何清單! + + %1$s 已經移動到 %2$s + 身份驗證無法運作? + 這裡有一些可能會有幫助的檢查:\n\n + - 檢查站台名稱沒有拼字錯誤\n\n + - 檢查您的站台是否仍在運行\n\n + - 若您使用雙因素驗證 (2FA),請使用下方的連結(填寫站台名稱後)\n\n + - 您也可以在不使用 2FA 的狀況下使用此連結\n\n + - 若還是無法運作,請在 Framagit 上回報問題,網址為 https://framagit.org/tom79/fedilab/issues + + 媒體已載入。點選這裡以顯示。 + 此動作可能會很久,您將在完成時收到通知。 + 仍在執行中,請稍候…… + 匯出狀態 + %1$s 的匯出狀態 + %2$s 中的 %1$s 個嘟文已匯出。 + 在匯出 %1$s 資料時發生錯誤 + 匯出資料時出錯! + 匯入資料時出錯! + + 代理伺服器 + 啟用代理伺服器? + 主機 + + 使用者名稱 + 密碼 + 在分享時新增嘟文詳細資訊 + 在 Liberpay 上支援應用程式 + 在正規表達式中有錯誤! + 在此站台上找不到時間軸! + 刪除此站台? + 翻譯成 + 追蹤站台 + 您已經追蹤此站台了! + 站台已被追蹤! + 夥伴 + 資訊 + 隱藏由 %s 轉嘟的嘟文 + 在個人資料頁面上推薦 + 顯示由 %s 轉嘟的嘟文 + 不要在個人資料頁面上推薦 + 帳號已在個人資料頁面上推薦 + 帳號不再於個人資料頁面上推薦 + 現在會顯示轉嘟的嘟文了! + 現在會隱藏轉嘟的嘟文了! + 直接訊息 + 過濾器 + 沒有過濾器。您可以透過點選 \"+\" 按鈕來建立一個。 + 關鍵字或詞組 + 本地時間軸 + 公開時間軸 + 通知 + 對話 + 不論是文字框中或在嘟文中的內容警告都將會符合。 + 丟棄而非隱藏 + 已過濾的嘟文將會不可逆地消失,即便過濾移除後也一樣 + 當關鍵字與詞組僅包含字母與數字時,它只會在整個單字都符合時才會套用 + 整個單字 + 過濾內容 + 過濾器應該套用的一個或多個內容 + 多久後過期 + 刪除過濾器? + 更新過濾器 + 建立過濾器 + 要關注誰 + 目前沒有在列表中的帳號! + 關注 + 選取全部 + 取消選取全部 + %s 已被關注! + 正在建立列表 %s + 新增帳號到列表 + 帳號已新增至列表 + 正在新增帳號到列表 + 您尚未建立列表。點選 \"+\" 按鈕以新增一個新的。 + 要關注誰 + Trunk API + 無法關注帳號 + 正在擷取遠端帳號 + 自動展開隱藏的媒體 + 新關注 + 新轉嘟 + 新最愛 + 新提及 + 投票結束 + 新嘟文 + 嘟文備份 + 新貼文 + 媒體下載 + 變更通知音效 + 選取鈴聲 + 啟用時間帶 + 操作方法的影片 + 正在擷取遠端嘟文串! + 沒有封鎖的網域! + 解除封鎖網域 + 您確定要解除封鎖 %s? + 您確定要封鎖 %s? + 已封鎖的網域 + 封鎖網域 + 網域已被封鎖 + 網域不再被封鎖! + 正在擷取遠端狀態 + 評論 + PeerTube 站台 + 使用右上角的按鈕來成為第一個對這部影片留下評論的人! + %s 次檢視 + 長度:%s + 新增站台 + 此影片未啟用評論! + 選擇解析度 + Peertube 最愛 + 影片已加入到書籤中! + 影片已從書籤中移除! + 您的最愛中沒有 PeerTube 影片! + 頻道 + 影片 + 頻道 + 使用 Emoji One + 資訊 + 在所有嘟文中顯示預覽 + 新的 UX/UI 設計師 + 顯示影片預覽 + 帳號 id 已複製到剪貼簿! + 變更語言 + 預設語言 + 截斷長嘟文 + 截斷超過「x」行的嘟文。0 代表停用。 + 顯示更多 + 顯示較少 + 管理標籤 + 標籤已存在! + 標籤已儲存! + 標籤已變更! + 標籤已刪除! + 排程轉嘟 + 已排程轉嘟! + 尚無排程轉嘟! + 排程轉嘟。]]> + 藝術時間軸 + 開啟選單 + 返回 + 應用程式圖示 + 個人檔案照片 + 個人檔案封面照片 + 聯絡站台的管理員 + 加入新的 + MastoHost 圖示 + 顏文字選擇器 + 重新整理 + 展開對話 + 移除帳號 + 刪除已封鎖的網域 + 自訂顏文字選擇器 + 播放影片 + 新嘟文 + 卡片的圖片 + 隱藏媒體 + 網站圖示 + 新增媒體描述 + + 永不 + 30 分鐘 + 1 小時 + 6 小時 + 12 小時 + 1 天 + 1 週 + + 在此欄位,您必須填寫您的站台主機名稱。\n舉例來說,如果您在 https://mastodon.social 上建立帳號\n只要填寫 mastodon.social(不要填寫 https://)\n + 您可以填寫首字母,然後就會有建議的名稱。\n\n + ⚠ 登入按鈕將只在站台名稱有效且站台正常運作時才會運作! + + 更多資訊 + + 語言 + 僅媒體 + 顯示 NSFW + Crowdin 翻譯 + Crowdin 管理員 + 應用程式翻譯 + 關於 Crowdin + 機器人 + Pixelfed 站台 + Mastodon 站台 + 這些裡面的任何一個 + 這些全部 + 這些都不要 + 這些字詞的其中一個(以空格分開) + 這些字詞全部(以空格分開) + 新增要過濾的字詞(以空格分隔) + 變更欄位名稱 + Misskey 站台 + 您的裝置上沒有安裝任何支援此連結的應用程式。 + 訂閱 + 概覽 + 趨勢 + 最近新增 + 本機 + 上傳 + 回覆 + 刪除留言 + 您想要刪除此留言嗎? + 全螢幕影片 + 影片模式 + 選取要上傳的檔案 + 我的影片 + 標題 + 授權條款 + 分類 + 語言 + 這個影片包含成人或裸露的內容 + 啟用影片留言 + 更新影片 + 描述 + 影片已更新! + 上傳已取消! + 影片已上傳! + 正在上傳,請稍候…… + 點選這裡以編輯影片資料。 + 刪除影片 + 您確定要刪除此部影片嗎? + 顯示 NSFW 影片 + 沒有影片 + 留下評論 + 分享 + 選擇排程模式 + 從裝置 + 從伺服器 + 嘟文(伺服器) + 嘟文(裝置) + 修改 + 在「擷取更多」按鈕之上顯示新嘟文 + 時間軸 + 界面 + 聯絡人 + %1$s 已在您的影片留言 %2$s]]> + %1$s 追蹤您的頻道 %2$s]]> + %1$s 追蹤您的帳號]]> + %1$s 已發佈]]> + %1$s 成功]]> + %1$s 失敗]]> + %1$s 發佈了新影片:%2$s]]> + %1$s 已被加入黑名單]]> + %1$s 已被解除黑名單]]> + 匯出資料 + 匯入資料 + 選取要匯入的檔案 + 選擇備份檔時發生錯誤! + 新增公開留言 + 傳送留言 + 沒有網路連線。您的訊息已經儲存為草稿。 + 純文字 + HTML + Markdown + 登出帳號 + 全部 + 幫助改進應用程式 + Open Collective 讓群組可以快速設定集合,募集資金並以透明的方式管理。 + 複製連結 + 連線 + 正常 + 緊湊 + 主控臺 + 設定顯示模式 + 修補程式安全提供者 + 更新追蹤網域 + 追蹤資料庫已更新! + 應用程式阻擋了 http 呼叫 + 阻擋呼叫清單 + 遞交 + 資料庫已匯出! + 特色主題標籤 + 以標籤過濾時間軸 + 沒有標籤 + 在通知分頁隱藏刪除通知按鈕 + 如果網址是從其他應用程式分享的,則接收其中繼資料 + + 投票 + 投票 + 建立投票 + 第一種選擇 + 第二種選擇 + 第 %d 種選擇 + 建立投票至少需要兩個選擇! + 完成 + 在 %s 結束投票 + 重新整理投票 + 投票 + 您曾投過的投票已經結束 + 您所嘟過的投票已經結束 + 自訂 + 分類 + 時段 + 進階 + 在未讀的嘟文上顯示「新」的標章 + PeerTube + 移動時間軸 + 隱藏時間軸 + 重新排列時間軸 + 已永久刪除列表 + 追蹤的站台遭移除 + 已移除釘選標籤 + 復原 + 您必須保留兩個可見的分頁! + 重新排列時間軸 + 主時間軸只能被隱藏! + BBCode + 一律將媒體標記為敏感訊息 + GNU 站台 + 已快取嘟文 + 在回覆中轉發標籤 + 長按以儲存媒體 + 將敏感內容模糊化 + 以清單顯示時間軸 + 顯示時間軸 + 在嘟文中標記機器人帳號 + 管理標籤 + 記住首頁時間軸的位置 + 歷史紀錄 + 播放清單 + 顯示名稱 + 您沒有任何播放清單。按一下「+」圖示以新增播放清單 + 您必須提供顯示名稱! + 當播放清單公開時,頻道為必填。 + 建立播放清單 + 目前播放清單中還沒有東西。 + 重作 + 媒體庫 + 表情符號 + 貼圖 + 橡皮擦 + 文字 + 過濾 + 筆刷 + 確定不儲存影像離開? + 捨棄 + 儲存中… + 影像成功儲存! + 無法儲存影像 + 不透明度 + 啟用圖片編輯器 + 新增投票項目 + 移除末尾投票項目 + 靜音對話 + 取消靜音對話 + 已取消此對話的靜音! + 此對話已經靜音 + 開啟應用程式功能 + 定時靜音 + 提及此帳號 + 重新快取 + 提及此嘟文 + 新聞 + 一般 + 地區性 + 藝術 + 新聞 + 行動 + 遊戲 + 科技 + 成人內容 + 怪獸 + 食物 + 站台圖示 + 在檢查可用站台時發生了些問題! + 加入 Mastodon + 透過挑選分類,然後按下按鈕來選擇站台。 + 按一下勾選按鈕以選擇站台。 + %1$s 位使用者 + 確認密碼 + 我同意 %1$s 和 %2$s + 伺服器規則 + 服務條款 + 註冊 + 本站台是以邀請制運作。您的帳號需要先由任一管理員手動核准通過才能使用。 + 請填寫所有欄位! + 密碼不相符! + 電子郵件似乎是無效的! + 您的使用者名稱在 %1$s 將會是獨一無二的 + 您將會收到一封確認用的電子郵件 + 使用至少 8 個字元 + 密碼應該包含至少 8 個字元 + 使用者名稱應僅包含字母、數字與底線 + 已建立帳號! + 已建立您的帳號!\n\n + 請在接下來的 48 小時內驗證您的電子郵件。\n\n + 您現在可以在第一欄中填寫 %1$s 並按下連結來連結到您的帳號。\n\n + 重要:如果您的站台需要驗證,您將會在驗證完成後收到電子郵件! + + 將訊息儲存至草稿? + 管理系統 + 報告 + 沒有要顯示的報告! + 重新連結帳號 + 應用程式無法存取管理功能。您可能需要重新連結帳號才能有正確的功能。 + 尚未解決 + 遠端 + 活躍 + 等待中 + 已停用 + 已靜音 + 已暫停 + 權限 + 電子信箱狀態 + 登入狀態 + 已加入 + 最近登入 IP + 警告 + 停用 + 靜音 + 透過電子郵件通知使用者 + 自訂警告 + 使用者 + 版主 + 管理員 + 已確認 + 未確認 + 已回報嘟文 + 帳號 + 復原靜音 + 復原停用 + 暫停 + 復原暫停 + 這個帳號已經靜音! + 這個帳號已取消靜音! + 這個帳號已經暫停! + 這個帳號已取消暫停! + 這個帳號已經停用! + 這個帳號已取消停用! + 這個帳號已被警告! + 顯示管理選單 + 在嘟文中顯示管理功能 + 允許 + 這個帳號已被核准! + 這個帳號已被拒絕! + 分配給我 + 取消分配 + 標記解決 + 標記未解決 + 內容為空! + 顯示 Fedilab 功能按鈕 + 應用程式需要存取音訊錄製 + 音訊訊息 + 啟用快速回覆 + 您回覆的帳號可能看不到您的訊息! + 若停用,App 將永遠會載入最新嘟文 + 若停用,敏感媒體將會隱藏並顯示按鈕 + 在預覽中長按可以儲存完整大小的媒體 + 在右上角新增一個按鈕,列出所有標籤/站台/清單 + 在該時段,應用程式將會傳送通知。您可以使用右側的微調器來反轉(亦即讓其安靜)此時段。 + 在個人資料圖片下顯示 Fedilab 按鈕。這是存取應用程式內功能的快捷鍵。 + 允許在時間軸中直接回覆狀態 + 將不會裁切時間軸中的預覽畫面。 + 允許直接在時間軸內播放嵌入影片 + 點擊擷取更多按鈕後,允許反轉讀取狀態的顯示方式 + 此選項可支援最近的加密套裝軟體。它對較舊的 Android 裝置或無法連線至您的站台時很有用。 + 專為 Peertube 影片而設。如果您無法讀取它們的話,請切換此模式。 + 這些標籤將允許從個人資料過濾狀態。您必須使用情境選單來查看它們。 + 在提及後自動插入斷行符號以將首字母大寫 + 允許內容創作者將狀態分享至他們的 RSS feed + 組成 + 上傳媒體時的最大重試次數 + 在此建立新資料夾 + 輸入資料夾名稱 + 請輸入有效的資料夾名稱 + 此資料夾已存在。\n請為此資料夾提供另一個名字 + 選取 + 預設目錄 + 資料夾 + 建立資料夾 + 在動作完成後顯示快顯訊息(轉嘟、最愛等等)? + 靜音的站台已匯出! + 新增站台 + 匯出站台 + 匯入站台 + 當機回報 + 啟用當機回報 + 若啟用,將會在本機建立當機回報,然後您就可以分享它了。 + Fedilab 已停止 :( + 您可以透過電子郵件傳送當機報告給我。這可以協助我修復它 :)\n\n您可以加入鵝外的內容。感謝您! + 使用所見即所得 + 啟用時,您將可以使用工具很簡單地格式化您的文字。 + 統計 + 總狀態數 + 轉嘟數 + 最愛的數量 + 提及的數量 + 追蹤的數量 + 投票的數量 + 回覆數 + 狀態數 + 狀態 + 能見度 + 有媒體的數量 + 有敏感媒體的數量 + CW 數量 + 第一個狀態的日期 + 最後一個狀態的日期 + 第一個通知日期 + 最後通知日期 + 頻率 + %s 每日狀態數 + 每天 %s 個通知 + 日期範圍 + 群組 + 沒有群組! + 停用自訂 Emoji 動畫 + 圖表 + 顯示圖表 + 應用程式蒐集您的本機資料中,請稍候…… + 備份 + 自動備份狀態 + 此選項是每個帳號分開生效的。它將會啟動一個會自動將您的狀態儲存到本機資料庫的服務。這樣就可以取得統計資料與圖表 + 自動備份通知 + 此選項是每個帳號分開生效的。它會啟動一個會自動將您的通知儲存到本機資料庫的服務。讓您可以取得統計資料與圖表 + 回報帳號 + 傳送邀請 + 您的站臺不允許註冊新帳號! + + %d 人投票 + + + %d 位投票者 + + + 單種選擇 + 多種選擇 + + + 5 分鐘 + 30 分鐘 + 1 小時 + 6 小時 + 1 天 + 3 天 + 7 天 + + + Torrent + Webview + + 要加入我的站臺「%1$s」,您可以在這些地方下載 Fedilab:\n\nF-Droid:%2$s\nGoogle:%3$s\n\n然後用 Fedilab 開啟下方的連結並建立您的帳號 :)\n\n%4$s + 您的投票不能有重覆的選項! + 給所有帳號 + 資料庫快取 + 清除您的家時間軸快取 + 清除您已快取的狀態 + 清除您的書籤 + 在快取中的檔案 + 總通知 + 隱藏選單項目 + Fedilab 正在執行即時通知 + 為 %1$s 帳號的 %2$s 事件 + %1$s 的即時通知 + 即時通知將只會為此帳號停用。 + 離開時清除快取 + 快取(媒體、已快取的訊息、從內建瀏覽器而來的資料)將會在離開應用程式時自動清除。 + 您想要取消追蹤此帳號嗎? + 在取消追蹤前顯示確認對話框 + 使用 Invidio.us 取代 Youtube + Invidious 是一個 YouTube 的替代前端 + 輸入您的自訂主機或留空以使用 invidio.us + 使用 Nitter 取代 Twitter + Nitter 是一個尊重隱私的開放原始碼替代 Twitter 前端。 + 輸入您的自訂主機或留空以使用 nitter.net + 將 Instagram 以 Bibliogram 取代 + Bibliogram 是一個尊重隱私的開放原始碼替代 Instagram 前端。 + 輸入您的自訂主機或留空以使用 bibliogram.art + 用 Libreddit 取代 Reddit + Libreddit 是一個尊重隱私的開放原始碼替代 Reddit 前端。 + 輸入您的自訂主機或留空以使用 libredd.it + 取代 Medium 連結 + 使用專注於隱私的開放原始碼替代前端來取代 medium.com 連結。 + 預設值:scribe.rip + 取代維基百科連結 + 使用專注於隱私的開放原始碼替代前端來取代維基百科連結。 + 預設值:wikiless.org + 隱藏 Fedilab 通知列 + 要隱藏狀態列中的其餘通知,點擊眼睛圖示的按鈕上然後取消勾選「在狀態列中顯示」 + 使用推播通知系統來取得即時通知。 + 沒有即時通知 + 即時通知 + 將會每15分鐘擷取一次通知。 + 新增註記 + 帳號註記 + 允許將較大的相片壓縮成尺寸較小的相片,且只有非常少或可忽略不計的圖片品質損失。 + 允許在維持品質的同時壓縮影片。 + 應用程式正在壓縮媒體,可能會很久…… + 變更應用程式圖示 + 按一下變更應用程式圖示 + 張貼 + 嘟文可見度 + 按一下這裡新增照片 + 可用格式:JPEG、PNG、GIF \n\n 最大檔案大小:15 MB \n\n 相簿可以包含最多 4 張照片或影片 + 上傳媒體 + 新增可選標題 + 應用程式從 API %1$s 收到非常長的錯誤訊息 + 訊息預覽 + 在每個訊息中新增提及 + 正在擷取對話 + 排序依 + 影片標題 + 加入 Peertube + 我至少 16 歲並同意此站台的 %1$s + 連結 + 在訊息中變更連結的顏色(URL、提及、標籤等等) + 轉發標頭 + 更改訊息頂部的顯示名稱色彩 + 更改訊息頂部的使用者名稱色彩 + 變更轉發標頭的顏色 + 嘟文 + 時間軸中嘟文的背景顏色 + 重設顏色 + 按一下這裡重設所有自訂顏色 + 重設 + 圖示 + 時間軸底部圖示的顏色 + 釘選此標籤 + 站台圖示 + 編輯個人資料 + 做動作 + 翻譯 + 圖片預覽 + 文字色彩 + 變更嘟文/轉嘟/訊息等的文字色彩 + 套用變更 + 您必須重新啟動應用程式以套用變更 + 重新啟動 + 使用自訂佈景主題 + 允許覆寫在上方選定的佈景主題的顏色 + 主題 + 儲存之前 + 佈景主題已匯出 + 佈景主題已成功匯出為 CSV + 將主要色彩套用到狀態列上 + 狀態列色彩 + 恢復預設佈景主題 + 匯入佈景主題 + 點擊這裡以從先前匯出的檔案匯入佈景主題 + 匯出佈景主題 + 點擊這裡以匯出目前的佈景主題 + 選取佈景主題檔案時遇到錯誤 + 佈景主題挑選程式 + 選取預先安裝的佈景主題 + 佈景主題 + 將主要顏色套用到導覽列上 + 導航列顏色 + 應用程式內容的基礎顏色。 + 背景色彩 + 選取部份使用者界面的強調色彩。 + 強調色彩 + 最常顯示在您的應用程式中。 + 主要色彩 + 匯出書籤到站臺 + 從站臺匯入書籤 + 使用者數量 + 狀態數量 + 站臺數量 + 已阻擋 + 結束於 %s + %s 新鮮事 + 您可以追蹤我的帳號來取得更新 + 此站臺於 https://instances.social 上不可用 + 顯示完整連結 + 分享連結 + URL 已複製到剪貼簿 + 使用其他應用程式開啟 + 檢查重新導向 + 此 URL 不會重新導向 + %1$s \n\n重新導向到\n\n %2$s + 變更使用者代理字串 + 設定自訂的使用者代理字串或留空 + 允許自訂用於 api 呼叫或內建瀏覽器的使用者字串 + 移除 UTM 參數 + 應用程式將會在造訪連結前自動移除 UTM 參數 + 趨勢 + 目前趨勢 + %d 個正在談論 + Twitter 帳號(透過 Nitter) + 以空格分開的 Twitter 使用者名稱 + 身份證明 + 已驗證的身份 + 由 %1$s 驗證 (%2$s) + 刪除通知 + 顯示更多選項 + 這是一則 Pixelfed 動態 + 上傳媒體,它將會自動加入到您的 Pixelfed 動態。 + 媒體已成功新增到您的動態中! + 動作已停用 + 取消追蹤 + 發生錯誤,請檢查設定中的下載目錄設定。 + 公告 + 沒有公告! + 新增反應 + 在應用程式中使用您最愛的瀏覽器。取消勾選此功能以使用外部應用程式開啟連結。 + 以 MB 計的視訊快取,零代表沒有快取。 + 浮水印 + 自動在圖片底部加入浮水印。可以為每個帳號自訂文字。 + 找不到散佈者! + 您需要散佈者來接收推播通知。\n您可以在 %1$s 找到更多資訊。\n\n您也可以在設定中停用推播通知來忽略訊息。 + 選取散佈者 + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 000000000..5c73b561a --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,158 @@ + + + + + + + + #d9d9d9 + + + #f3f3f3 + #606984 + #ca8f04 + #2b90d9 + #00000000 + + #77000000 + #FFF + + #282c37 + #009688 + #F44336 + #B2DFDB + #FFCDD2 + #D7CCC8 + #000 + #222 + + #585c67 + #454b5b + #282c37 + #313543 + #9baec8 + #d9e1e8 + #efefef + #772b90d9 + #9FBEDD + #2b90d9 + + + + #313543 + #393f4f + + #000000 + + #ffffff + + #79bd9a + #5579bd9a + + + + #617684 + #d9d9d9 + #d9d9d9 + + + #617684 + + + #393f4f + #393f4f + + + #27a7fc + #27a7fc + #27a7fc + + + #2b90d9 + #F44336 + + #F1680D + + #c52404 + + #dc3545 + + #5E8D87 + #5F819D + #CC6666 + + #B3808080 + + + #0066FF + #AF593E + #01A368 + #FF861F + #ED0A3F + #FF3F34 + #76D7EA + #8359A3 + #FBE870 + #C5E17A + + + #144365 + + #14161B + #E0E0E0 + + + #D32F2F + #388E3C + #0288D1 + + + #D32F2F + #388E3C + #0288D1 + #512DA8 + + + #2e2e2e + #EF5350 + #42A5F5 + #66BB6A + #c8c8c8 + #c8c8c8 + + + #5E68A0 + + #0288D1 + #2b90d9 + + + #f3f3f3 + #606984 + + + #ff0000 + #ffa500 + #ffff00 + #008000 + #0000ff + #4b0082 + #ee82ee + + #E57373 + #BA68C8 + #FFD54F + #64B5F6 + #81C784 + #4DB6AC + #FF8A65 + #4DD0E1 + #AED581 + #9575CD + #FFB74D + #4FC3F7 + #DCE775 + #7986CB + #9E9E9E + + diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml new file mode 100644 index 000000000..b50136e1f --- /dev/null +++ b/app/src/main/res/values/dimens.xml @@ -0,0 +1,19 @@ + + + 16dp + 16dp + 8dp + 176dp + 16dp + 12dp + 2dp + 10dp + false + 180dp + 16dp + 5dp + 10dp + 50dp + 12sp + 35dp + \ No newline at end of file diff --git a/app/src/main/res/values/ic_launcher_background.xml b/app/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 000000000..c5d5899fd --- /dev/null +++ b/app/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #FFFFFF + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 000000000..9ddffcfdf --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,1589 @@ + + Fedilab + Open the menu + Close the menu + + About + About the instance + Privacy + Cache + Logout + Login + + + Close + Yes + No + Cancel + Download + Download %1$s + Media saved + File: %1$s + Password + Email + Accounts + Toots + Tags + Save + Restore + No results! + Instance + Instance: mastodon.social + Now works with the account %1$s + Add an account + The content of the toot has been copied to the clipboard + The URL of the toot has been copied to the clipboard + Change + Select a picture… + Clean + Camera + Delete all + Translate this toot. + Schedule + Text and icon sizes + Change the current text size: + Change the current icon size: + Next + Previous + Open with + Validate + Media + Share with + Shared via Fedilab + Replies + User name + Drafts + Favourites + New followers + Mentions + Boosts + Show boosts + Show replies + Open in browser + Translate + Please, wait few seconds before making this action. + + Home + Local timeline + Federated timeline + Options + Favourites + Communication + Muted users + Blocked users + Notifications + Follow requests + Settings + Remove an account + Remove the account %1$s from the application? + Send an email + Tap on the path to change it + Failed! + Scheduled toots + Information below may reflect the user\'s profile incompletely. + Insert emoji + The app did not collect custom emojis for the moment. + Push notifications + Are you sure you want to logout? + Are you sure you want to logout @%1$s@%2$s? + + No toot to display + No stories to display + Stories + Boosted by %1$s + + + Add this toot to your favourites? + Remove this toot from your favourites? + Boost this toot? + Unboost this toot? + Pin this toot? + Unpin this toot? + + Mute + Block + Report + Delete + Copy + Share + Mention + Timed mute + Delete & re-draft + + Mute this account? + Block this account? + Report this toot? + Block this domain? + Unmute this account? + Unblock this account? + + + + Notify + Silent + + + + Delete this toot? + Delete & re-draft this toot? + + + Bookmarks + Add to bookmarks + Remove bookmark + No bookmarks to display + Status has been added to bookmarks! + Status was removed from bookmarks! + + + %d s + %d m + %d h + %d d + + + %d second + %d seconds + + + %d minute + %d minutes + + + %d hour + %d hours + + + + %d day + %d days + + + + Warning + What is on your mind? + TOOT! + QUEET! + cw + Write a toot + Reply to a toot + Write a queet + Reply to a queet + Select a media + An error occurred while selecting the media! + Remove this media? + Your toot is empty! + Visibility of the toot + Visibility of the toots by default: + The toot has been sent! + You are replying to this toot: + Sensitive content? + + Post to public timelines + Do not post to public timelines + Post to followers only + Post to mentioned users only + + + No drafts! + Choose a toot + Choose an account + Select some accounts + Delete draft? + Tap on the button to display the original toot + + Describe for the visually impaired + + No description available! + + + Release %1$s + Developer: + License: + GNU GPL V3 + Source code: + FramaGit + Translation of toots: + Powered by Yandex.Translate + Search instances: + instances.social + Icon designer: + + Conversation + + + No account to display + No follow request + Toots \n %1$s + Following \n %1$s + Followers \n %1$s + Pinned \n %d + Authorize + Reject + + + + No scheduled toots to display! + Write a toot and then choose Schedule from the top menu. + Delete scheduled toot? + Media: %d + The toot has been scheduled! + The scheduled date must be greater than the current hour! + Battery saver is enabled! It might not work as expected. + + The time for muting should be greater than one minute. + %1$s has been muted until %2$s.\n You can unmute this account from their profile page. + %1$s is muted until %2$s.\n Tap here to unmute the account. + + No notification to display + mentioned you + wrote a new message + boosted your status + favourited your status + followed you + asked to follow you + + and another notification + and %d other notifications + + + %d like + %d likes + + Delete a notification? + Delete all notifications? + The notification has been deleted! + All notifications have been deleted! + + Following + Followers + Pinned + + Unable to get client id! + Unable to connect to instance domain! + No Internet connection! + The account was blocked! + The account is no longer blocked! + The account was muted! + The account is no longer muted! + The account was followed! + The account is no longer followed! + The toot was boosted! + The toot is no longer boosted! + The toot was added to your favourites! + The toot was removed from your favourites! + The toot was reported! + The toot was deleted! + The toot was pinned! + The toot was unpinned! + Oops ! An error occurred! + An error occurred! The instance did not return an authorisation code! + The instance domain does not seem to be valid! + An error occurred while switching between accounts! + An error occurred while searching! + The profile data have been saved! + No action can be taken + The media has been saved! + An error occurred while translating! + Translations are disabled in settings + Draft saved! + Are you sure this instance allows this number of characters? Usually, this value is close to 500 characters. + Visibility of the toots has been changed for the account %1$s + + Number of toots per load + Number of accounts per load + Number of notifications per load + + Always + WIFI + Ask + Load the media + Load the pictures + Show more… + Show less… + Sensitive content + Disable GIF avatars + Path: + Save drafts automatically + Add URL of media in toots + Notify when someone follows you + Notify when someone boosts your status + Notify when someone favourites your status + Notify when someone mentions you + Notify when a poll ended + Notify for new posts + Show confirmation dialog before boosting + Show confirmation dialog before adding to favourites + Notify in WIFI only + Notify? + Silent Notifications + NSFW view timeout (seconds, 0 means off) + Media Description timeout (seconds, 0 means off) + Edit profile + Custom sharing + Your custom sharing URL… + Bio… + Lock account + Save changes + Choose a header picture + Fit preview images + Automatically split toots in replies when chars are over: + You have reached the 160 characters allowed! + You have reached the 30 characters allowed! + + Between + and + The time must be greater than %1$s + The time must be lower than %1$s + Start time + End time + Use the built-in browser + Custom tabs + Enable Javascript + Automatically expand cw + Allow third-party cookies + + + + LibreTranslate + Yandex + DeepL + Systran + + + Your API key, you can leave blank for Yandex + + + Dark + Light + Black + + + + Dark + Light + + + Set LED colour: + + + Blue + Cyan + Magenta + Green + Red + Yellow + White + + + Follow + s + Unblock + Mute + Unmute + Request sent + Follows you + + Search + First letter in capital for replies + Resize pictures + Resize videos + + + Push notifications + + Please, confirm push notifications that you want to receive. + You can enable or disable these notifications later in settings (Notifications tab). + + + Clear cache + There are %1$s of data in cache.\n\nWould you like to delete them? + Mb + Cache was cleared! %1$s were released + + + Title + Title… + Description + Keywords + Keywords… + + + Synchronize + Filter + Your toots + Your notifications + Public + Unlisted + Private + Direct + Some keywords… + Show media + Show pinned + No matching result found! + Backup toots for %1$s + %1$s new toots have been imported + %1$s new notifications have been imported + + + Dates descending + Dates ascending + + + + No + Only + Both + + No toots were found in database. Please, use the synchronize button from the menu to retrieve them. + + Recorded data + + Only basic information from accounts are stored on the device. + These data are strictly confidential and can only be used by the application. + Deleting the application immediately removes these data.\n + ⚠ Login and passwords are never stored. They are only used during a secure authentication (SSL) with an instance. + + + Permissions: + + - ACCESS_NETWORK_STATE: Used to detect if the device is connected to a WIFI network.\n + - INTERNET: Used for queries to an instance.\n + - WRITE_EXTERNAL_STORAGE: Used to store media or to move the app on a SD card.\n + - READ_EXTERNAL_STORAGE: Used to add media to toots.\n + - BOOT_COMPLETED: Used to start the notification service.\n + - WAKE_LOCK: Used during the notification service. + + + API permissions: + + - Read: Read data.\n + - Write: Post statuses and upload media for statuses.\n + - Follow: Follow, unfollow, block, unblock.\n\n + ⚠ These actions are carried out only when user requests them. + + Tracking and Libraries + + The application does not use tracking tools (audience measurement, error reporting, etc.) and does not contain any advertising.\n\n + The use of libraries is minimized: \n + - Glide: To manage media\n + - Android-Job: To manage services\n + - PhotoView: To manage images\n + + + Translation of toots + + The application offers the ability to translate toots using the locale of the device and the Yandex API.\n + Yandex has its proper privacy-policy which can be found here: https://yandex.ru/legal/confidential/?lang=en + + + + + Thank you to: + + + Filter out by regular expressions + Search + Delete + + Fetch more toots… + + + + Lists + Are you sure you want to permanently delete this list? + There is nothing in this list yet. When members of this list post new statuses, they will appear here. + Add to list + Add list + Delete list + Edit list + New list title + The account was added to the list! + You don\'t have any lists yet! + + %1$s has moved to %2$s + + Authentication does not work? + + Here are some checks that might help:\n\n + - Check there is no spelling mistakes in the instance name\n\n + - Check that your instance is not down\n\n + - If you use the two-factor authentication (2FA), please use the link at the bottom (once the instance name is filled)\n\n + - You can also use this link without using the 2FA\n\n + - If it still does not work, please raise an issue on FramaGit at https://framagit.org/tom79/fedilab/issues + + + Media has been loaded. Tap here to display it. + + This action can be quite long. You will be notified when it will be finished. + Still running, please wait… + Export statuses + Export statuses for %1$s + %1$s toots out of %2$s have been exported. + Something went wrong when exporting data for %1$s + Something went wrong when exporting data! + Something went wrong when importing data! + + Proxy + Enable proxy? + Host + Port + Login + Password + Add toot details when sharing + Support the app on Liberapay + There is an error in the regular expression! + No timelines was found on this instance! + Delete this instance? + Translate in + Follow instance + You already follow this instance! + The instance is followed! + Masto.host is our Mastodon hosting partner.\n\nWith this partnership they provide me free hosting for Mastalab\'s instance and in exchange I promote them in here.\n\nTo be clear, there is no money involved or tracking of users. I needed an instance for testing, we talked and agreed on this.\n\nGo check them out if you want to run your own Mastodon instance. + Partnerships + Information + + Hide boosts from %s + Feature on profile + Show boosts from %s + Don\'t feature on profile + The account is now featured on profile + The account is no longer featured on profile + Boosts are now shown! + Boosts are now hidden! + Direct message + Filters + No filters to display. You can create one by tapping on the \"+\" button. + Keyword or phrase + Home timeline + Public timelines + Notifications + Conversations + Will be matched regardless of casing in text or content warning of a toot + Drop instead of hide + Filtered toots will disappear irreversibly, even if filter is later removed + When the keyword or phrase is alphanumeric only, it will only be applied if it matches the whole word + Whole word + Filter contexts + One or multiple contexts where the filter should apply + Expire after + Delete filter? + Update filter + Create filter + Whom to follow + There is no accounts listed for the moment! + Follow + Select all + Unselect all + %s is followed! + Creating the list %s + Adding accounts to the list + Accounts were added to the list + Adding accounts to the list + You have not created a list yet. Tap on the \"+\" button to add a new one. + Who to follow + Trunk API + Account(s) can\'t be followed + Fetching remote account + Automatically expand hidden media + + + + HTTP + SOCKS + + + New follow + New Boost + New Favourite + New Mention + Poll Ended + New Toot + Toots Backup + New posts + Media Download + Change notification sound + Select Tone + Enable time slot + How To Videos + Fetching remote thread! + No blocked domains! + Unblock domain + Are you sure to unblock %s? + Are you sure to block %s?\n\nYou will not see any content from that domain in any public timeline or in your notifications. Your followers from that domain will be removed. + Blocked domains + Block domain + The domain is blocked + The domain is no longer blocked! + Fetching remote status + Comment + Peertube instance + Be the first to leave a comment on this video with the top right button! + %s views + Duration: %s + Add an instance + Comments are not enabled on this video! + Pick up a resolution + Peertube favourites + The video has been added to bookmarks! + The video has been removed from bookmarks! + There is no Peertube videos in your favourites! + Channel + Videos + Channels + Use Emoji One + Information + Display previews in all toots + New UX/UI designer + Display video previews + The account id has been copied in the clipboard! + Change the language + Default language + Truncate long toots + Truncate toots over \'x\' lines. Zero means disabled. + Display more + Display less + Manage tags + The tag already exists! + The tag has been stored! + The tag has been changed! + The tag has been removed! + Schedule boost + The boost is scheduled! + No scheduled boost to display! + Schedule boost.]]> + Art timeline + Open menu + Go back + Logo of the application + Profile picture + Profile banner + Contact admin of the instance + Add new + MastoHost logo + Emoji picker + Refresh + Expand the conversation + Remove an account + Remove the blocked domain + Custom emoji picker + Play video + New toot + Image of the card + Hide media + Favicon + Add description for media (for the visually impaired) + + + Never + 30 minutes + 1 hour + 6 hours + 12 hours + 1 day + 1 week + + + + In this field, you need to write your instance domain.\nFor example, if you created your account on https://mastodon.social\nJust write mastodon.social\nYou can start writing first letters and names will be suggested. + + More information + + + + Languages + + Media only + Show NSFW + Crowdin translations + Crowdin manager + Translation of the application + About Crowdin + Bot + Pixelfed instance + Mastodon instance + Any of these + All of these + None of these + Any of these words (space-separated) + All these words (space-separated) + Add some words to filter (space-separated) + Change column name + Misskey instance + No app supporting this link is installed on your device. + Subscriptions + Overview + Trending + Recently added + Local + Upload + Reply + Delete a comment + Are you sure to delete this comment? + Full screen video + Mode for videos + Select the file to upload + My videos + Title + License + Category + Language + This video contains mature or explicit content + Enable video comments + Update video + Description + The video has been updated! + Upload cancelled! + The video has been uploaded! + Uploading, please wait… + Tap here to edit the video data. + Delete video + Are you sure to delete this video? + Display NSFW videos + No videos to display! + Leave a comment + Share + Choose a schedule mode + From device + From server + Toots (Server) + Toots (Device) + Modify + Display new toots above the \"Fetch more\" button + Timelines + Interface + Contacts + %1$s commented your video %2$s]]> + %1$s is following your channel %2$s]]> + %1$s is following your account]]> + + %1$s has been published]]> + %1$s succeeded]]> + %1$s failed]]> + %1$s published a new video: %2$s]]> + %1$s has been blacklisted]]> + %1$s has been unblacklisted]]> + Export data + Import Data + Select the file to import + An error occurred when selecting the backup file! + Add a public comment + Send comment + There is no Internet connection. Your message has been stored in drafts. + Plain text + HTML + Markdown + Logout account + All + Support the app + Open Collective enables groups to quickly set up a collective, raise funds and manage them transparently. + Copy link + Connect + Normal + Compact + Console + Set display mode + Patch the Security Provider + Update tracking domains + The tracking data base has been updated! + http calls blocked by the application + List of blocked calls + Submit + The data base has been exported! + Featured hashtags + Filter timeline with tags + No tags + Hide the "delete" button in the notification tab + Attach an image when sharing a URL + + + Poll + Polls + Create a poll + Choice 1 + Choice 2 + Choice %d + You need two choices at least for the poll! + Done + end at %s + Refresh poll + Vote + A poll you have voted in has ended + A poll you tooted has ended + Customize + Categories + Time slot + Advanced + Display \'new\' badge on unread toots + Peertube + Move timeline + Hide timeline + Reorder timelines + List permanently deleted + Followed instance removed + Pinned tag removed + Undo + You need to keep two visible tabs! + Reorder timelines + Main timelines can only be hidden! + BBCode + Always mark media as sensitive + GNU instance + Cached status + Forward tags in replies + Long press to store media + Blur sensitive media + Display timelines in a list + Display timelines + Mark bot accounts in toots + Manage tags + Remember the position in Home timeline + History + Playlists + Display name + You don\'t have any playlists. Tap on the \"+\" icon to add a new playlist + You must provide a display name! + The channel is required when the playlist is public. + Create a playlist + There is nothing in this playlist yet. + redo + Gallery + Emoji + Sticker + Eraser + Text + Filter + Brush + Are you sure you want to exit without saving the image? + Discard + Saving… + Image Saved Successfully! + Failed to save Image + Opacity + Enable photo editor + Add a poll item + Remove last poll item + Mute conversation + Unmute conversation + The conversation is no longer muted! + The conversation is muted + Open application features + Timed mute + Mention the account + Refresh cache + Mention the status + News + General + Regional + Art + Music + Journalism + Activism + Gaming + Technology + Adult content + Furry + Food + Logo of the instance + Something went wrong when checking available instances! + Join Mastodon + Select a category to get a list of instances, then tap on the instance you like. + Choose an instance by tapping on a check button. + %1$s users + Confirm password + I agree to %1$s and %2$s + server rules + terms of service + Sign up + This instance works with invitations. Your account will need to be manually approved by an administrator before being usable. + Please, fill all the fields! + This field cannot be empty! + Passwords don\'t match! + The email doesn\'t seem to be valid! + Your username will be unique on %1$s + You will be sent a confirmation e-mail + Use at least 8 characters + Password should contain at least 8 characters + Username should only contain letters, numbers and underscores + Account created! + + Your account has been created!\n\n + Think to validate your email within the 48 next hours.\n\n + You can now connect your account by writing %1$s in the first field and tap on Connect.\n\n + Important: If your instance required validation, you will receive an email once it is validated! + + Save the message in drafts? + Administration + Reports + No reports to display! + Reconnect the account + The application failed to access the admin features. You might need to reconnect the account for having the correct scope. + Unresolved + Remote + Active + Pending + Disabled + Silenced + Suspended + Permissions + Email status + Login status + Joined + Most recent IP + Warn + Disable + Silence + Notify the user per e-mail + Custom warning + User + Moderator + Administrator + Confirmed + Not confirmed + Reported statuses + Account + Undo silence + Undo disable + Suspend + Undo suspend + The account is silenced! + The account is no longer silenced! + The account is suspended! + The account is no longer suspended! + The account is disabled! + The account is no longer disabled! + The account has been warned! + Display the admin menu + Display the admin feature in statuses + Allow + The account is approved! + The account is rejected! + Assign to me + Unassign + Mark as resolved + Mark as unresolved + Empty content! + Display Fedilab features button + The application needs to access audio recording + Audio + Voice message + Enable quick reply + The account you are replying might not see your message! + If disabled, the app will always load last statuses + If disabled, sensitive media will be hidden with a button + Store media in full size with a long press on previews + Add an ellipse button at the top right for listing all tags/instances/lists + During the time slot, the app will send notifications. You can reverse (ie: silent) this time slot with the right spinner. + Display a Fedilab button below profile picture. It is a shortcut for accessing in-app features. + Allow to reply directly in timelines below statuses + Previews will not be cropped in timelines + Allow to play embedded videos directly in timelines + Allow to reverse the way to read statuses that are displayed once tapping the fetch more button + This option allows to support recent cipher suites. It is useful for older Android devices or if you cannot connect to your instance. + Exclusively for Peertube videos. Switch this mode if you cannot play them. + These tags will allow to filter statuses from profiles. You will have to use the context menu for seeing them. + Automatically insert a line break after the mention to capitalize the first letter + Allow content creators to share statuses to their RSS feeds + Compose + Maximum retry times when uploading media + Create a new Folder here + Enter the folder name + Please enter a valid folder name + This folder already exists.\n Please provide another name for the folder + Select + Default Directory + Folder + Create folder + Display a toast message after an action has been completed (boost, fav, etc.)? + Muted instances have been exported! + Add an instance + Export instances + Import instances + Crash reports + Enable crash reports + If enabled, a crash report will be created locally and then you will be able to share it. + Fedilab has stopped :( + You can send me by email the crash report. It will help to fix it :)\n\nYou can add additional content. Thank you! + Use the wysiwyg + When enabled, you will be able to format your text easily with tools. + Statistics + Total statuses + Number of boosts + Number of favourites + Number of mentions + Number of follows + Number of polls + Number of replies + Number of statuses + Statuses + Visibility + Number with media + Number with sensitive media + Number with CW + First status date + Last status date + First notification date + Last notification date + Frequency + %s statuses per day + %s notifications per day + Date range + Groups + No groups! + Disable custom animated emojis + Charts + Display charts + The application collects your local data, please wait… + Backup + Auto backup statuses + This option is per account. It will launch a service that will automatically store your statuses locally in the database. That allows to get statistics and charts + Auto backup notifications + This option is per account. It will launch a service that will automatically store your notifications locally in the database. That allows to get statistics and charts + Report account + Send an invitation + Your instance does not allow to register a new account! + + %d vote + %d votes + + + + %d voter + %d voters + + + Single choice + Multiple choices + + + 5 minutes + 30 minutes + 1 hour + 6 hours + 1 day + 3 days + 7 days + + + + Webview + Direct stream + + + Join a Fediverse instance + + For joining my instance \"%1$s\", you can download Fedilab:\n\nF-Droid: %2$s\nGoogle: %3$s\n\nThen open the link below with Fedilab and create your account :)\n\n%4$s + + Your poll can\'t have duplicated options! + For all accounts + Database cache + Clear your home timeline cache + Clear your cached statuses + Clear your bookmarks + Files in cache + Total notifications + Hide menu items + Fedilab is running live notifications + For %1$s accounts with %2$s events + Live notifications for %1$s + Live notifications will be enabled for this account. + Clear cache when leaving + The cache (media, cached messages, data from the built-in browser) will be automatically cleared when leaving the application. + Do you want to unfollow this account? + Show confirmation dialog before unfollowing + + + YouTube + Use an alternative frontend for YouTube + YouTube frontend domain + + Twitter + Use an alternative frontend for Twitter + Twitter frontend domain + + Instagram + Use an alternative frontend for Instagram + Instagram frontend domain + + Reddit + Use an alternative frontend for Reddit + Reddit frontend domain + + Medium + Use an alternative frontend for Medium + Medium frontend domain + + Wikipedia + Replace Wikipedia link with an open source alternative front-end focused on privacy. + Wikipedia frontend domain + + Hide Fedilab notification bar + For hiding the remaining notification in the status bar, tap on the eye icon button then uncheck: \"Display in status bar\" + Use a push notifications system for getting notifications in real time. + No live notifications + Live notifications + Notifications will be fetched every 15 minutes. + Add notes + Notes for the account + Allow to compress large photos into smaller sized photos with very less or negligible loss in quality of the image. + Allow compressing videos while maintaining their quality. + The app is compressing the media, it can be quite long… + Change app icon + Tap to change the app icon + Post + Visibility of the post + Tap here to add photos + Accepted Formats: jpeg, png, gif \n\nMax File Size: 15 MB \n\nAlbums can contain up to 4 photos or videos + Upload media + Add an optional caption + The app received a very long error message from the API %1$s + Message preview + Add mentions in each message + Fetching conversation + Order by + Title for the video + Join Peertube + I am at least 16 years old and agree to the %1$s of this instance + Links + Change the color of links (URLs, mentions, tags, etc.) in messages + Reblogs header + Change the color of display name at the top of messages + Change the color of the user name at the top of messages + Change the color of the header for reblogs + Posts + Background color of posts in timelines + Reset colors + Tap here to reset all your custom colors + Reset + Icons + Color of bottom icons in timelines + Pin this tag + Logo of the instance + Edit profile + Make an action + Translation + Image preview + Text color + Change the text color in pots + Apply changes + You need to restart the application to apply changes + Restart + Use a custom theme + Allow to override colors of the selected theme above + Theming + Store before + The theme was exported + The theme has been successfully exported in CSV + Apply the primary color to the status bar + Status bar color + Restore a default theme + Import a theme + Tap here to import a theme from a previous export + Export the theme + Tap here to export the current theme + An error occurred when selecting the theme file + + Theme Picker + Select a pre-installed theme + Themes + Apply the primary color to the navigation bar + Navigation bar color + The underlying color of the app’s content. + Background color + Accents select parts of the UI. + Accent color + Displayed most frequently across your app. + Primary color + + + Export bookmarks to the instance + Import bookmarks from the instance + User count + Status count + Instance count + Blocked + End in %s + What\'s new in %s + You can follow my account for updates + This instance is not available on https://instances.social + Display full link + Share link + The URL has been copied to the clipboard + Open with another app + Check redirect + This URL does not redirect + %1$s \n\nredirects to\n\n %2$s + Change the user agent + Set a custom user agent or leave blank + Allows to customize the user agent used for api calls or with the built-in browser. + Remove UTM parameters + The app will automatically remove UTM parameters from URLs before visiting a link. + Trends + Trending now + %d people talking + Twitter accounts (via Nitter) + Twitter usernames space separated + Identity proofs + Verified identity + Verified by %1$s (%2$s) + Delete the notification + Display more options + It is a Pixelfed story + Upload a media, it will be automatically added to your Pixelfed story. + Media successfully added to your story! + Action disabled + Unfollow + Something went wrong, please check your download directory in settings. + Announcements + No announcements! + Add a reaction + Use your favourite browser inside the app. Uncheck this feature to open links externally. + Video cache in MB, zero means no cache. + Watermarks + Automatically add a watermark at the bottom of pictures. The text can be customized for each account. + No distributors found! + You need a distributor for receiving push notifications.\nYou will find more details at %1$s.\n\nYou can also disable push notifications in settings for ignoring that message. + Select a distributor + Continue + Custom + The instance does not seem to be valid! + Boosted by + Favourited by + Followers only + Other + Images + Eg.: Sensitive Content + Add status + Remove status + Posting message… + Sending message %d/%d + activity_porfile_pp + Is up! + Is down! + version: %s \n %s users - %s statuses + Checked at: %s + Uptime: %,.2f %% + ]]> + Stop recording + + + + Reporting %1$s + Tell us what\'s going with this post + Choose the best match + I don\'t like it + It is not something you want to see + It\'s spam + Malicious links, fake engagement, or repetetive replies + It violates server rules + You are aware that it breaks specific rules + It\'s something else + The issue does not fit into other categories + + Don\'t want to see this? + Here are your options for controlling what you see on Mastodon: + Unfollow %1$s + You are following this account. To not see their posts in your home feed anymore, unfollow them. + Mute %1$s + You will not see their posts. They can still follow you and see your posts and will not know that they are muted. + Block %1$s + You will not see their posts. They will not be able to see your posts or follow you. They will be able to tell that they are blocked. + Are there any posts that back up this report? + Which rules are being violated? + + Select all that apply + + Is there anything else you think we should know? + Additional comments + The account is from another server. Send an anonymized copy of the report there as well? + Forward to %1$s + Report has been sent! + Don\'t have an account? + Join the fediverse + Hi! We invite you to join the Fediverse. + \"Mastodon isn’t a single website like Twitter or Facebook, it\'s a network of thousands of communities operated by different organizations and individuals that provide a seamless social media experience.\" + \"PeerTube, developed by Framasoft, is the free and decentralized alternative to video platforms, providing you over 400,000 videos published by 60,000 users and viewed over 15 million times\" + Mentions + Favourites + Reblogs + Poll results + Updates from people + Follows + Clear all notifications + Mark all notifications as read + Display all categories + Are you sure you want to delete all notifications? It can\'t be undone. + Interactions + Add filter + Add Field + Unlocked + Locked + Save changes + Bot account + Account discoverable + Delete field + Are you sure you want to delete that field? + Profile has been updated! + List name is not valid! + No accounts found for this list! + Scheduled + + + Push notifications + Fetch at fixed times + No notifications + + + + SET_COOKIES + SET_PROXY_ENABLED + SET_PROXY_TYPE + SET_PROXY_HOST + SET_PROXY_PORT + SET_EMBEDDED_BROWSER + SET_DEFAULT_LOCALE_NEW + SET_SEND_CRASH_REPORTS + SET_DISABLE_GIF + SET_JAVASCRIPT + SET_CUSTOM_USER_AGENT + SET_BLUR_SENSITIVE + SET_NSFW_TIMEOUT + SET_VIDEO_NSFW + SET_NOTIFICATION_TYPE + + PUSH_NOTIFICATIONS + REPEAT_NOTIFICATIONS + NO_NOTIFICATIONS + + SET_ALLOW_NOTIFICATION + SET_PICTURE_COMPRESSED + SET_VIDEO_COMPRESSED + SET_FORWARD_TAGS_IN_REPLY + SET_NOTIF_SOUND + SET_ENABLE_TIME_SLOT + SET_CLEAR_CACHE_EXIT + SET_DISPLAY_EMOJI + SET_DISPLAY_CARD + SET_DISPLAY_VIDEO_PREVIEWS + SET_NOTIFICATION_ACTION + SET_DISPLAY_CONTENT_AFTER_FM + SET_FEATURED_TAGS + SET_FEATURED_TAG_ACTION + SET_HIDE_DELETE_BUTTON_ON_TAB + SET_RETRIEVE_METADATA_IF_URL_FROM_EXTERAL + + + public + unlisted + private + direct + + + + en + fr + de + it + ja + zh-TW + zh-CN + eu + ar + nl + gl + el + pt + es + pl + sr + uk + ru + no + kab + ca + szl + sc + + + + English + Français + Deutsch + Italiano + 日本語 + 繁體中文 + 简体中文 + Euskara + العربية + Nederlands + Galego + Ελληνικά + Português + Español + Polski + Српски + Українська + Русский + Norsk + Taqbaylit + Català + ślůnski + Sarda + + + + 0 + 1 + 2 + 3 + 4 + 5 + 6 + + + + 0 + 1 + + + + + DARK + LIGHT + + + SET_LONG_PRESS_MEDIA + SET_DISPLAY_TIMELINE_IN_LIST + SET_UNFOLLOW_VALIDATION + SET_ONION_SCHEME + SET_REMEMBER_POSITION_HOME + SET_DISPLAY_ADMIN_MENU + SET_DISPLAY_ADMIN_STATUSES + SET_AUTO_BACKUP_STATUSES + SET_AUTO_BACKUP_NOTIFICATIONS + SET_LED_COLOUR_VAL + SET_SHOW_BOOSTS + SET_SHOW_REPLIES + SET_DISABLE_ANIMATED_EMOJI + SET_CAPITALIZE + SET_WYSIWYG + LOGO_LAUNCHER + SET_FULL_PREVIEW + SET_SHARE_DETAILS + SET_CUSTOM_SHARING + SET_CUSTOM_SHARING_URL + set_attachment_action + SET_THEME + SET_TIME_FROM + SET_TIME_TO + SET_AUTO_STORE + SET_POPUP_PUSH + SET_POPUP_RELEASE_NOTES + SET_MED_DESC_TIMEOUT + SET_MEDIA_URLS + SET_TRANSLATOR + + + SET_TRANS_FORCED + SET_NOTIFY + SET_NOTIF_FOLLOW + SET_NOTIF_ADD + SET_NOTIF_MENTION + SET_NOTIF_SHARE + SET_NOTIF_FAVOURITE + SET_NOTIF_POLL + SET_NOTIF_MEDIA + SET_NOTIF_STATUS + SET_NOTIF_FOLLOW_FILTER + SET_NOTIF_ADD_FILTER + SET_NOTIF_MENTION_FILTER + SET_NOTIF_SHARE_FILTER + SET_NOTIF_POLL_FILTER + SET_NOTIF_STATUS_FILTER + SET_FILTER_REGEX_HOME + SET_FILTER_REGEX_LOCAL + SET_FILTER_REGEX_PUBLIC + SET_NOTIF_VALIDATION + SET_NOTIF_VALIDATION_FAV + SET_WIFI_ONLY + SET_NOTIF_SILENT + SET_EXPAND_CW + SET_DISPLAY_ALL_NOTIFICATIONS_TYPE + SET_EXCLUDED_NOTIFICATIONS_TYPE + SET_EXPAND_MEDIA + SET_PHOTO_EDITOR + MAX_UPLOAD_IMG_RETRY_TIMES + SET_DISPLAY_NEW_BADGE + SET_DISPLAY_BOT_ICON + SET_DISPLAY_CONFIRM + SET_CUSTOM_TABS + SET_FOLDER_RECORD + SET_TOOT_VISIBILITY + SET_DISPLAY_LOCAL + SET_DISPLAY_GLOBAL + SET_AUTOMATICALLY_SPLIT_TOOTS + SET_AUTOMATICALLY_SPLIT_TOOTS_SIZE + SET_TRUNCATE_TOOTS_SIZE + SET_ART_WITH_NSFW + SET_SECURITY_PROVIDER + SET_ALLOW_STREAM + SET_VIDEO_CACHE + SET_WATERMARK + SET_WATERMARK_TEXT + SET_SHOW_ACCOUNT_BOOSTS + SET_SHOW_ACCOUNT_REPLIES + SET_PROXY_PASSWORD + SET_PROXY_LOGIN + SET_ACCOUNTS_PER_CALL + SET_STATUSES_PER_CALL + SET_NOTIFICATIONS_PER_CALL + + SET_INVIDIOUS + SET_INVIDIOUS_HOST + invidious.snopyta.org + + SET_FILTER_UTM + SET_NITTER + SET_NITTER_HOST + nitter.net + + SET_BIBLIOGRAM + SET_BIBLIOGRAM_HOST + bibliogram.art + + SET_LIBREDDIT + SET_LIBREDDIT_HOST + libredd.it + + REPLACE_MEDIUM + REPLACE_MEDIUM_HOST + scribe.rip + + REPLACE_WIKIPEDIA + REPLACE_WIKIPEDIA_HOST + wikiless.org + LAST_NOTIFICATION_MAX_ID + Type of notifications + Alternative frontends + Chose the type of notifications + Notification options + Notification sounds + Disable notifications + During this time slot + Base of the theme + Chose if the base of the theme should be dark or light + Allow to create your custom theme + Customize timelines + Themes from contributors + Pick-up a theme that has been built by contributors + Select a theme + + Etiam congue dictum urna, eget dapibus arcu pellentesque sit amet. Ut at nulla placerat, aliquet turpis sit amet, finibus lacus. Aenean eget eros in risus feugiat ornare. Aenean mi massa, ornare nec molestie in, varius ac nisl. Vestibulum molestie, nulla nec elementum rhoncus, sapien nulla suscipit felis, ac auctor est. + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin quis odio malesuada enim dignissim consectetur. Nunc nec tincidunt ipsum. Aliquam bibendum id felis at pulvinar. Etiam tincidunt ante elit, eu tristique mauris aliquam id. Nulla eget sapien sit amet augue aliquet porttitor eget quis urna. + Customize main colors + More Actions + Types of notifications to display + Confirm unfollows + + + diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml new file mode 100644 index 000000000..130d83913 --- /dev/null +++ b/app/src/main/res/values/styles.xml @@ -0,0 +1,203 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/pref_administration.xml b/app/src/main/res/xml/pref_administration.xml new file mode 100644 index 000000000..5a5d2cdc9 --- /dev/null +++ b/app/src/main/res/xml/pref_administration.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/pref_compose.xml b/app/src/main/res/xml/pref_compose.xml new file mode 100644 index 000000000..24237bc98 --- /dev/null +++ b/app/src/main/res/xml/pref_compose.xml @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/pref_interface.xml b/app/src/main/res/xml/pref_interface.xml new file mode 100644 index 000000000..943c94cb4 --- /dev/null +++ b/app/src/main/res/xml/pref_interface.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/pref_language.xml b/app/src/main/res/xml/pref_language.xml new file mode 100644 index 000000000..c15f0f1b5 --- /dev/null +++ b/app/src/main/res/xml/pref_language.xml @@ -0,0 +1,14 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/pref_notifications.xml b/app/src/main/res/xml/pref_notifications.xml new file mode 100644 index 000000000..a37cf73b9 --- /dev/null +++ b/app/src/main/res/xml/pref_notifications.xml @@ -0,0 +1,146 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/pref_privacy.xml b/app/src/main/res/xml/pref_privacy.xml new file mode 100644 index 000000000..f43663471 --- /dev/null +++ b/app/src/main/res/xml/pref_privacy.xml @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/pref_theming.xml b/app/src/main/res/xml/pref_theming.xml new file mode 100644 index 000000000..de51c5010 --- /dev/null +++ b/app/src/main/res/xml/pref_theming.xml @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/pref_timelines.xml b/app/src/main/res/xml/pref_timelines.xml new file mode 100644 index 000000000..39663e870 --- /dev/null +++ b/app/src/main/res/xml/pref_timelines.xml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/playstore/AndroidManifest.xml b/app/src/playstore/AndroidManifest.xml new file mode 100644 index 000000000..3cce1103b --- /dev/null +++ b/app/src/playstore/AndroidManifest.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/playstore/google-services.json b/app/src/playstore/google-services.json new file mode 100644 index 000000000..90e22f9ea --- /dev/null +++ b/app/src/playstore/google-services.json @@ -0,0 +1,39 @@ +{ + "project_info": { + "project_number": "479837431022", + "project_id": "pc-api-4835782490875392372-140", + "storage_bucket": "pc-api-4835782490875392372-140.appspot.com" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:479837431022:android:1102a97a55202beb547fff", + "android_client_info": { + "package_name": "app.fedilab.android" + } + }, + "oauth_client": [ + { + "client_id": "479837431022-mettpakdcso72c35djvikfc57l4i7n53.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyCklTEEgLUxy__0Vzcr5_H179kYPXGjmGo" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "479837431022-mettpakdcso72c35djvikfc57l4i7n53.apps.googleusercontent.com", + "client_type": 3 + } + ] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/app/src/playstore/java/app/fedilab/android/activities/MainActivity.java b/app/src/playstore/java/app/fedilab/android/activities/MainActivity.java new file mode 100644 index 000000000..a2a8c4bf9 --- /dev/null +++ b/app/src/playstore/java/app/fedilab/android/activities/MainActivity.java @@ -0,0 +1,34 @@ +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ +package app.fedilab.android.activities; + + +import com.kobakei.ratethisapp.RateThisApp; + +import app.fedilab.android.BaseMainActivity; + + +public class MainActivity extends BaseMainActivity { + + @Override + protected void rateThisApp() { + RateThisApp.onCreate(this); + RateThisApp.Config config = new RateThisApp.Config(3, 10); + RateThisApp.init(config); + RateThisApp.showRateDialogIfNeeded(this); + } + + +} diff --git a/app/src/playstore/java/app/fedilab/android/services/EmbeddedDistrib.java b/app/src/playstore/java/app/fedilab/android/services/EmbeddedDistrib.java new file mode 100644 index 000000000..531b915f2 --- /dev/null +++ b/app/src/playstore/java/app/fedilab/android/services/EmbeddedDistrib.java @@ -0,0 +1,11 @@ +package app.fedilab.android.services; + + +import org.unifiedpush.android.embedded_fcm_distributor.EmbeddedDistributorReceiver; + +public class EmbeddedDistrib extends EmbeddedDistributorReceiver { + public EmbeddedDistrib() { + super(new HandlerFCM()); + } + +} \ No newline at end of file diff --git a/app/src/playstore/java/app/fedilab/android/services/HandlerFCM.java b/app/src/playstore/java/app/fedilab/android/services/HandlerFCM.java new file mode 100644 index 000000000..7e410214f --- /dev/null +++ b/app/src/playstore/java/app/fedilab/android/services/HandlerFCM.java @@ -0,0 +1,18 @@ +package app.fedilab.android.services; + +import android.content.Context; + +import androidx.annotation.Nullable; + +import org.jetbrains.annotations.NotNull; +import org.unifiedpush.android.embedded_fcm_distributor.GetEndpointHandler; + + +public class HandlerFCM implements GetEndpointHandler { + + @Override + public @NotNull String getEndpoint(@Nullable Context context, @NotNull String token, @NotNull String instance) { + return "https://gotify.fedilab.app/FCM?token=" + token + "&instance=" + instance; + } + +} \ No newline at end of file diff --git a/app/src/playstore/res/xml/file_paths.xml b/app/src/playstore/res/xml/file_paths.xml new file mode 100644 index 000000000..08135b5ac --- /dev/null +++ b/app/src/playstore/res/xml/file_paths.xml @@ -0,0 +1,11 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/test/java/app/fedilab/android/ExampleUnitTest.java b/app/src/test/java/app/fedilab/android/ExampleUnitTest.java new file mode 100644 index 000000000..7d868088c --- /dev/null +++ b/app/src/test/java/app/fedilab/android/ExampleUnitTest.java @@ -0,0 +1,17 @@ +package app.fedilab.android; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + @Test + public void addition_isCorrect() { + assertEquals(4, 2 + 2); + } +} \ No newline at end of file diff --git a/autoimageslider/.gitignore b/autoimageslider/.gitignore new file mode 100644 index 000000000..796b96d1c --- /dev/null +++ b/autoimageslider/.gitignore @@ -0,0 +1 @@ +/build diff --git a/autoimageslider/build.gradle b/autoimageslider/build.gradle new file mode 100644 index 000000000..3acabfb77 --- /dev/null +++ b/autoimageslider/build.gradle @@ -0,0 +1,39 @@ +apply plugin: 'com.android.library' + + +android { + compileSdkVersion 31 + + defaultConfig { + minSdkVersion 15 + targetSdkVersion 31 + versionCode 5 + versionName "1.4.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + +} + +// Add a new configuration to hold your dependencies +configurations { + libConfig +} + +dependencies { + implementation fileTree(include: ['*.jar'], dir: 'libs') + //noinspection GradleCompatible + implementation 'androidx.appcompat:appcompat:1.4.1' + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test:runner:1.4.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + +} diff --git a/autoimageslider/proguard-rules.pro b/autoimageslider/proguard-rules.pro new file mode 100644 index 000000000..f1b424510 --- /dev/null +++ b/autoimageslider/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/autoimageslider/src/androidTest/java/com/smarteist/autoimageslider/ExampleInstrumentedTest.java b/autoimageslider/src/androidTest/java/com/smarteist/autoimageslider/ExampleInstrumentedTest.java new file mode 100644 index 000000000..72b6b6bf8 --- /dev/null +++ b/autoimageslider/src/androidTest/java/com/smarteist/autoimageslider/ExampleInstrumentedTest.java @@ -0,0 +1,27 @@ +package com.smarteist.autoimageslider; + +import static org.junit.Assert.assertEquals; + +import android.content.Context; + +import androidx.test.InstrumentationRegistry; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getTargetContext(); + + assertEquals("com.smarteist.autoimageslider.test", appContext.getPackageName()); + } +} diff --git a/autoimageslider/src/main/AndroidManifest.xml b/autoimageslider/src/main/AndroidManifest.xml new file mode 100644 index 000000000..058ee0393 --- /dev/null +++ b/autoimageslider/src/main/AndroidManifest.xml @@ -0,0 +1,12 @@ + + + + + + + diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/IndicatorManager.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/IndicatorManager.java new file mode 100644 index 000000000..bf1a8d0bb --- /dev/null +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/IndicatorManager.java @@ -0,0 +1,46 @@ +package com.smarteist.autoimageslider.IndicatorView; + +import androidx.annotation.Nullable; + +import com.smarteist.autoimageslider.IndicatorView.animation.AnimationManager; +import com.smarteist.autoimageslider.IndicatorView.animation.controller.ValueController; +import com.smarteist.autoimageslider.IndicatorView.animation.data.Value; +import com.smarteist.autoimageslider.IndicatorView.draw.DrawManager; +import com.smarteist.autoimageslider.IndicatorView.draw.data.Indicator; + +public class IndicatorManager implements ValueController.UpdateListener { + + private final DrawManager drawManager; + private final AnimationManager animationManager; + private final Listener listener; + + IndicatorManager(@Nullable Listener listener) { + this.listener = listener; + this.drawManager = new DrawManager(); + this.animationManager = new AnimationManager(drawManager.indicator(), this); + } + + public AnimationManager animate() { + return animationManager; + } + + public Indicator indicator() { + return drawManager.indicator(); + } + + public DrawManager drawer() { + return drawManager; + } + + @Override + public void onValueUpdated(@Nullable Value value) { + drawManager.updateValue(value); + if (listener != null) { + listener.onIndicatorUpdated(); + } + } + + interface Listener { + void onIndicatorUpdated(); + } +} diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/PageIndicatorView.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/PageIndicatorView.java new file mode 100644 index 000000000..ce3c06dbe --- /dev/null +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/PageIndicatorView.java @@ -0,0 +1,648 @@ +package com.smarteist.autoimageslider.IndicatorView; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.content.Context; +import android.database.DataSetObserver; +import android.graphics.Canvas; +import android.os.Build; +import android.os.Parcelable; +import android.util.AttributeSet; +import android.util.Pair; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewParent; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.text.TextUtilsCompat; +import androidx.core.view.ViewCompat; +import androidx.viewpager.widget.PagerAdapter; +import androidx.viewpager.widget.ViewPager; + +import com.smarteist.autoimageslider.IndicatorView.animation.type.IndicatorAnimationType; +import com.smarteist.autoimageslider.IndicatorView.animation.type.ScaleAnimation; +import com.smarteist.autoimageslider.IndicatorView.draw.controller.DrawController; +import com.smarteist.autoimageslider.IndicatorView.draw.data.Indicator; +import com.smarteist.autoimageslider.IndicatorView.draw.data.Orientation; +import com.smarteist.autoimageslider.IndicatorView.draw.data.PositionSavedState; +import com.smarteist.autoimageslider.IndicatorView.draw.data.RtlMode; +import com.smarteist.autoimageslider.IndicatorView.utils.CoordinatesUtils; +import com.smarteist.autoimageslider.IndicatorView.utils.DensityUtils; +import com.smarteist.autoimageslider.IndicatorView.utils.IdUtils; +import com.smarteist.autoimageslider.InfiniteAdapter.InfinitePagerAdapter; +import com.smarteist.autoimageslider.SliderPager; + +public class PageIndicatorView extends View implements SliderPager.OnPageChangeListener, IndicatorManager.Listener, SliderPager.OnAdapterChangeListener { + + private IndicatorManager manager; + private DataSetObserver setObserver; + private SliderPager viewPager; + private boolean isInteractionEnabled; + + public PageIndicatorView(Context context) { + super(context); + init(null); + } + + public PageIndicatorView(Context context, AttributeSet attrs) { + super(context, attrs); + init(attrs); + } + + public PageIndicatorView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(attrs); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public PageIndicatorView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(attrs); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + findViewPager(getParent()); + } + + @Override + protected void onDetachedFromWindow() { + unRegisterSetObserver(); + super.onDetachedFromWindow(); + } + + @Override + public Parcelable onSaveInstanceState() { + Indicator indicator = manager.indicator(); + PositionSavedState positionSavedState = new PositionSavedState(super.onSaveInstanceState()); + positionSavedState.setSelectedPosition(indicator.getSelectedPosition()); + positionSavedState.setSelectingPosition(indicator.getSelectingPosition()); + positionSavedState.setLastSelectedPosition(indicator.getLastSelectedPosition()); + + return positionSavedState; + } + + @Override + public void onRestoreInstanceState(Parcelable state) { + if (state instanceof PositionSavedState) { + Indicator indicator = manager.indicator(); + PositionSavedState positionSavedState = (PositionSavedState) state; + indicator.setSelectedPosition(positionSavedState.getSelectedPosition()); + indicator.setSelectingPosition(positionSavedState.getSelectingPosition()); + indicator.setLastSelectedPosition(positionSavedState.getLastSelectedPosition()); + super.onRestoreInstanceState(positionSavedState.getSuperState()); + + } else { + super.onRestoreInstanceState(state); + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + Pair pair = manager.drawer().measureViewSize(widthMeasureSpec, heightMeasureSpec); + setMeasuredDimension(pair.first, pair.second); + } + + @Override + protected void onDraw(Canvas canvas) { + manager.drawer().draw(canvas); + } + + @SuppressLint("ClickableViewAccessibility") + @Override + public boolean onTouchEvent(MotionEvent event) { + manager.drawer().touch(event); + return true; + } + + @Override + public void onIndicatorUpdated() { + invalidate(); + } + + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + onPageScroll(position, positionOffset); + } + + @Override + public void onPageSelected(int position) { + onPageSelect(position); + } + + @Override + public void onPageScrollStateChanged(int state) { + if (state == ViewPager.SCROLL_STATE_IDLE) { + manager.indicator().setInteractiveAnimation(isInteractionEnabled); + } + } + + @Override + public void onAdapterChanged(@NonNull SliderPager viewPager, @Nullable PagerAdapter oldAdapter, @Nullable PagerAdapter newAdapter) { + updateState(); + } + + public int getCount() { + return manager.indicator().getCount(); + } + + public void setCount(int count) { + if (count >= 0 && manager.indicator().getCount() != count) { + manager.indicator().setCount(count); + updateVisibility(); + requestLayout(); + } + } + + public void setDynamicCount(boolean dynamicCount) { + manager.indicator().setDynamicCount(dynamicCount); + + if (dynamicCount) { + registerSetObserver(); + } else { + unRegisterSetObserver(); + } + } + + public int getRadius() { + return manager.indicator().getRadius(); + } + + public void setRadius(int radiusDp) { + if (radiusDp < 0) { + radiusDp = 0; + } + + int radiusPx = DensityUtils.dpToPx(radiusDp); + manager.indicator().setRadius(radiusPx); + invalidate(); + } + + public void setRadius(float radiusPx) { + if (radiusPx < 0) { + radiusPx = 0; + } + + manager.indicator().setRadius((int) radiusPx); + invalidate(); + } + + public int getPadding() { + return manager.indicator().getPadding(); + } + + public void setPadding(int paddingDp) { + if (paddingDp < 0) { + paddingDp = 0; + } + + int paddingPx = DensityUtils.dpToPx(paddingDp); + manager.indicator().setPadding(paddingPx); + invalidate(); + } + + public void setPadding(float paddingPx) { + if (paddingPx < 0) { + paddingPx = 0; + } + + manager.indicator().setPadding((int) paddingPx); + invalidate(); + } + + public float getScaleFactor() { + return manager.indicator().getScaleFactor(); + } + + public void setScaleFactor(float factor) { + if (factor > ScaleAnimation.MAX_SCALE_FACTOR) { + factor = ScaleAnimation.MAX_SCALE_FACTOR; + + } else if (factor < ScaleAnimation.MIN_SCALE_FACTOR) { + factor = ScaleAnimation.MIN_SCALE_FACTOR; + } + + manager.indicator().setScaleFactor(factor); + } + + public int getStrokeWidth() { + return manager.indicator().getStroke(); + } + + public void setStrokeWidth(float strokePx) { + int radiusPx = manager.indicator().getRadius(); + + if (strokePx < 0) { + strokePx = 0; + + } else if (strokePx > radiusPx) { + strokePx = radiusPx; + } + + manager.indicator().setStroke((int) strokePx); + invalidate(); + } + + public void setStrokeWidth(int strokeDp) { + int strokePx = DensityUtils.dpToPx(strokeDp); + int radiusPx = manager.indicator().getRadius(); + + if (strokePx < 0) { + strokePx = 0; + + } else if (strokePx > radiusPx) { + strokePx = radiusPx; + } + + manager.indicator().setStroke(strokePx); + invalidate(); + } + + public int getSelectedColor() { + return manager.indicator().getSelectedColor(); + } + + public void setSelectedColor(int color) { + manager.indicator().setSelectedColor(color); + invalidate(); + } + + public int getUnselectedColor() { + return manager.indicator().getUnselectedColor(); + } + + public void setUnselectedColor(int color) { + manager.indicator().setUnselectedColor(color); + invalidate(); + } + + public void setAutoVisibility(boolean autoVisibility) { + if (!autoVisibility) { + setVisibility(VISIBLE); + } + + manager.indicator().setAutoVisibility(autoVisibility); + updateVisibility(); + } + + + public void setOrientation(@Nullable Orientation orientation) { + if (orientation != null) { + manager.indicator().setOrientation(orientation); + requestLayout(); + } + } + + public long getAnimationDuration() { + return manager.indicator().getAnimationDuration(); + } + + public void setAnimationDuration(long duration) { + manager.indicator().setAnimationDuration(duration); + } + + public void setAnimationType(@Nullable IndicatorAnimationType type) { + manager.onValueUpdated(null); + + if (type != null) { + manager.indicator().setAnimationType(type); + } else { + manager.indicator().setAnimationType(IndicatorAnimationType.NONE); + } + invalidate(); + } + + + public void setInteractiveAnimation(boolean isInteractive) { + manager.indicator().setInteractiveAnimation(isInteractive); + this.isInteractionEnabled = isInteractive; + } + + + public void setViewPager(@Nullable SliderPager pager) { + releaseViewPager(); + if (pager == null) { + return; + } + + viewPager = pager; + viewPager.addOnPageChangeListener(this); + viewPager.addOnAdapterChangeListener(this); + manager.indicator().setViewPagerId(viewPager.getId()); + + setDynamicCount(manager.indicator().isDynamicCount()); + updateState(); + } + + + public void releaseViewPager() { + if (viewPager != null) { + viewPager.removeOnPageChangeListener(this); + viewPager = null; + } + } + + + public void setRtlMode(@Nullable RtlMode mode) { + Indicator indicator = manager.indicator(); + if (mode == null) { + indicator.setRtlMode(RtlMode.Off); + } else { + indicator.setRtlMode(mode); + } + + if (viewPager == null) { + return; + } + + int selectedPosition = indicator.getSelectedPosition(); + int position = selectedPosition; + + if (isRtl()) { + position = (indicator.getCount() - 1) - selectedPosition; + + } else if (viewPager != null) { + position = viewPager.getCurrentItem(); + } + + indicator.setLastSelectedPosition(position); + indicator.setSelectingPosition(position); + indicator.setSelectedPosition(position); + invalidate(); + } + + + public int getSelection() { + return manager.indicator().getSelectedPosition(); + } + + + public void setSelection(int position) { + Indicator indicator = manager.indicator(); + position = adjustPosition(position); + + if (position == indicator.getSelectedPosition() || position == indicator.getSelectingPosition()) { + return; + } + + indicator.setInteractiveAnimation(false); + indicator.setLastSelectedPosition(indicator.getSelectedPosition()); + indicator.setSelectingPosition(position); + indicator.setSelectedPosition(position); + manager.animate().basic(); + } + + public void setSelected(int position) { + Indicator indicator = manager.indicator(); + IndicatorAnimationType animationType = indicator.getAnimationType(); + indicator.setAnimationType(IndicatorAnimationType.NONE); + + setSelection(position); + indicator.setAnimationType(animationType); + } + + + public void clearSelection() { + Indicator indicator = manager.indicator(); + indicator.setInteractiveAnimation(false); + indicator.setLastSelectedPosition(Indicator.COUNT_NONE); + indicator.setSelectingPosition(Indicator.COUNT_NONE); + indicator.setSelectedPosition(Indicator.COUNT_NONE); + manager.animate().basic(); + } + + + public void setProgress(int selectingPosition, float progress) { + Indicator indicator = manager.indicator(); + if (!indicator.isInteractiveAnimation()) { + return; + } + + int count = indicator.getCount(); + if (count <= 0 || selectingPosition < 0) { + selectingPosition = 0; + + } else if (selectingPosition > count - 1) { + selectingPosition = count - 1; + } + + if (progress < 0) { + progress = 0; + + } else if (progress > 1) { + progress = 1; + } + + if (progress == 1) { + indicator.setLastSelectedPosition(indicator.getSelectedPosition()); + indicator.setSelectedPosition(selectingPosition); + } + + indicator.setSelectingPosition(selectingPosition); + manager.animate().interactive(progress); + } + + public void setClickListener(@Nullable DrawController.ClickListener listener) { + manager.drawer().setClickListener(listener); + } + + private void init(@Nullable AttributeSet attrs) { + setupId(); + initIndicatorManager(attrs); + } + + private void setupId() { + if (getId() == NO_ID) { + setId(IdUtils.generateViewId()); + } + } + + private void initIndicatorManager(@Nullable AttributeSet attrs) { + manager = new IndicatorManager(this); + manager.drawer().initAttributes(getContext(), attrs); + + Indicator indicator = manager.indicator(); + indicator.setPaddingLeft(getPaddingLeft()); + indicator.setPaddingTop(getPaddingTop()); + indicator.setPaddingRight(getPaddingRight()); + indicator.setPaddingBottom(getPaddingBottom()); + isInteractionEnabled = indicator.isInteractiveAnimation(); + } + + private void registerSetObserver() { + if (setObserver != null || viewPager == null || viewPager.getAdapter() == null) { + return; + } + + setObserver = new DataSetObserver() { + @Override + public void onChanged() { + updateState(); + } + }; + + try { + viewPager.getAdapter().registerDataSetObserver(setObserver); + } catch (IllegalStateException e) { + e.printStackTrace(); + } + } + + private void unRegisterSetObserver() { + if (setObserver == null || viewPager == null || viewPager.getAdapter() == null) { + return; + } + + try { + viewPager.getAdapter().unregisterDataSetObserver(setObserver); + setObserver = null; + } catch (IllegalStateException e) { + e.printStackTrace(); + } + } + + private void updateState() { + if (viewPager == null || viewPager.getAdapter() == null) { + return; + } + int count; + int position; + if (viewPager.getAdapter() instanceof InfinitePagerAdapter) { + count = ((InfinitePagerAdapter) viewPager.getAdapter()).getRealCount(); + if (count > 0) { + position = viewPager.getCurrentItem() % count; + } else { + position = 0; + } + } else { + count = viewPager.getAdapter().getCount(); + position = viewPager.getCurrentItem(); + } + + int selectedPos = isRtl() ? (count - 1) - position : position; + manager.indicator().setSelectedPosition(selectedPos); + manager.indicator().setSelectingPosition(selectedPos); + manager.indicator().setLastSelectedPosition(selectedPos); + manager.indicator().setCount(count); + manager.animate().end(); + + updateVisibility(); + requestLayout(); + } + + private void updateVisibility() { + if (!manager.indicator().isAutoVisibility()) { + return; + } + + int count = manager.indicator().getCount(); + int visibility = getVisibility(); + + if (visibility != VISIBLE && count > Indicator.MIN_COUNT) { + setVisibility(VISIBLE); + + } else if (visibility != INVISIBLE && count <= Indicator.MIN_COUNT) { + setVisibility(View.INVISIBLE); + } + } + + private void onPageSelect(int position) { + Indicator indicator = manager.indicator(); + boolean canSelectIndicator = isViewMeasured(); + int count = indicator.getCount(); + + if (canSelectIndicator) { + if (isRtl()) { + position = (count - 1) - position; + } + + setSelection(position); + } + } + + private void onPageScroll(int position, float positionOffset) { + Indicator indicator = manager.indicator(); + IndicatorAnimationType animationType = indicator.getAnimationType(); + boolean interactiveAnimation = indicator.isInteractiveAnimation(); + boolean canSelectIndicator = isViewMeasured() && interactiveAnimation && animationType != IndicatorAnimationType.NONE; + + if (!canSelectIndicator) { + return; + } + + Pair progressPair = CoordinatesUtils.getProgress(indicator, position, positionOffset, isRtl()); + int selectingPosition = progressPair.first; + float selectingProgress = progressPair.second; + setProgress(selectingPosition, selectingProgress); + } + + private boolean isRtl() { + switch (manager.indicator().getRtlMode()) { + case On: + return true; + + case Off: + return false; + + case Auto: + return TextUtilsCompat.getLayoutDirectionFromLocale(getContext().getResources().getConfiguration().locale) == ViewCompat.LAYOUT_DIRECTION_RTL; + } + + return false; + } + + private boolean isViewMeasured() { + return getMeasuredHeight() != 0 || getMeasuredWidth() != 0; + } + + private void findViewPager(@Nullable ViewParent viewParent) { + boolean isValidParent = viewParent != null && + viewParent instanceof ViewGroup && + ((ViewGroup) viewParent).getChildCount() > 0; + + if (!isValidParent) { + return; + } + + int viewPagerId = manager.indicator().getViewPagerId(); + SliderPager viewPager = findViewPager((ViewGroup) viewParent, viewPagerId); + + if (viewPager != null) { + setViewPager(viewPager); + } else { + findViewPager(viewParent.getParent()); + } + } + + @Nullable + private SliderPager findViewPager(@NonNull ViewGroup viewGroup, int id) { + if (viewGroup.getChildCount() <= 0) { + return null; + } + + View view = viewGroup.findViewById(id); + if (view != null && view instanceof SliderPager) { + return (SliderPager) view; + } else { + return null; + } + } + + private int adjustPosition(int position) { + Indicator indicator = manager.indicator(); + int count = indicator.getCount(); + int lastPosition = count - 1; + + if (position <= 0) { + position = 0; + + } else if (position > lastPosition) { + position = lastPosition; + } + + return position; + } +} diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/animation/AnimationManager.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/animation/AnimationManager.java new file mode 100644 index 000000000..27ccdeadc --- /dev/null +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/animation/AnimationManager.java @@ -0,0 +1,36 @@ +package com.smarteist.autoimageslider.IndicatorView.animation; + +import androidx.annotation.NonNull; + +import com.smarteist.autoimageslider.IndicatorView.animation.controller.AnimationController; +import com.smarteist.autoimageslider.IndicatorView.animation.controller.ValueController; +import com.smarteist.autoimageslider.IndicatorView.draw.data.Indicator; + + +public class AnimationManager { + + private final AnimationController animationController; + + public AnimationManager(@NonNull Indicator indicator, @NonNull ValueController.UpdateListener listener) { + this.animationController = new AnimationController(indicator, listener); + } + + public void basic() { + if (animationController != null) { + animationController.end(); + animationController.basic(); + } + } + + public void interactive(float progress) { + if (animationController != null) { + animationController.interactive(progress); + } + } + + public void end() { + if (animationController != null) { + animationController.end(); + } + } +} diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/animation/controller/AnimationController.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/animation/controller/AnimationController.java new file mode 100644 index 000000000..5d0cdc2bf --- /dev/null +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/animation/controller/AnimationController.java @@ -0,0 +1,296 @@ +package com.smarteist.autoimageslider.IndicatorView.animation.controller; + +import androidx.annotation.NonNull; + +import com.smarteist.autoimageslider.IndicatorView.animation.type.BaseAnimation; +import com.smarteist.autoimageslider.IndicatorView.animation.type.IndicatorAnimationType; +import com.smarteist.autoimageslider.IndicatorView.draw.data.Indicator; +import com.smarteist.autoimageslider.IndicatorView.draw.data.Orientation; +import com.smarteist.autoimageslider.IndicatorView.utils.CoordinatesUtils; + +public class AnimationController { + + private final ValueController valueController; + private final ValueController.UpdateListener listener; + private final Indicator indicator; + private BaseAnimation runningAnimation; + private float progress; + private boolean isInteractive; + + public AnimationController(@NonNull Indicator indicator, @NonNull ValueController.UpdateListener listener) { + this.valueController = new ValueController(listener); + this.listener = listener; + this.indicator = indicator; + } + + public void interactive(float progress) { + this.isInteractive = true; + this.progress = progress; + animate(); + } + + public void basic() { + this.isInteractive = false; + this.progress = 0; + animate(); + } + + public void end() { + if (runningAnimation != null) { + runningAnimation.end(); + } + } + + private void animate() { + IndicatorAnimationType animationType = indicator.getAnimationType(); + switch (animationType) { + case NONE: + listener.onValueUpdated(null); + break; + + case COLOR: + colorAnimation(); + break; + + case SCALE: + scaleAnimation(); + break; + + case WORM: + wormAnimation(); + break; + + case FILL: + fillAnimation(); + break; + + case SLIDE: + slideAnimation(); + break; + + case THIN_WORM: + thinWormAnimation(); + break; + + case DROP: + dropAnimation(); + break; + + case SWAP: + swapAnimation(); + break; + + case SCALE_DOWN: + scaleDownAnimation(); + break; + } + } + + private void colorAnimation() { + int selectedColor = indicator.getSelectedColor(); + int unselectedColor = indicator.getUnselectedColor(); + long animationDuration = indicator.getAnimationDuration(); + + BaseAnimation animation = valueController + .color() + .with(unselectedColor, selectedColor) + .duration(animationDuration); + + if (isInteractive) { + animation.progress(progress); + } else { + animation.start(); + } + + runningAnimation = animation; + } + + private void scaleAnimation() { + int selectedColor = indicator.getSelectedColor(); + int unselectedColor = indicator.getUnselectedColor(); + int radiusPx = indicator.getRadius(); + float scaleFactor = indicator.getScaleFactor(); + long animationDuration = indicator.getAnimationDuration(); + + BaseAnimation animation = valueController + .scale() + .with(unselectedColor, selectedColor, radiusPx, scaleFactor) + .duration(animationDuration); + + if (isInteractive) { + animation.progress(progress); + } else { + animation.start(); + } + + runningAnimation = animation; + } + + private void wormAnimation() { + int fromPosition = indicator.isInteractiveAnimation() ? indicator.getSelectedPosition() : indicator.getLastSelectedPosition(); + int toPosition = indicator.isInteractiveAnimation() ? indicator.getSelectingPosition() : indicator.getSelectedPosition(); + + int from = CoordinatesUtils.getCoordinate(indicator, fromPosition); + int to = CoordinatesUtils.getCoordinate(indicator, toPosition); + boolean isRightSide = toPosition > fromPosition; + + int radiusPx = indicator.getRadius(); + long animationDuration = indicator.getAnimationDuration(); + + BaseAnimation animation = valueController + .worm() + .with(from, to, radiusPx, isRightSide) + .duration(animationDuration); + + if (isInteractive) { + animation.progress(progress); + } else { + animation.start(); + } + + runningAnimation = animation; + } + + private void slideAnimation() { + int fromPosition = indicator.isInteractiveAnimation() ? indicator.getSelectedPosition() : indicator.getLastSelectedPosition(); + int toPosition = indicator.isInteractiveAnimation() ? indicator.getSelectingPosition() : indicator.getSelectedPosition(); + + int from = CoordinatesUtils.getCoordinate(indicator, fromPosition); + int to = CoordinatesUtils.getCoordinate(indicator, toPosition); + long animationDuration = indicator.getAnimationDuration(); + + BaseAnimation animation = valueController + .slide() + .with(from, to) + .duration(animationDuration); + + if (isInteractive) { + animation.progress(progress); + } else { + animation.start(); + } + + runningAnimation = animation; + } + + private void fillAnimation() { + int selectedColor = indicator.getSelectedColor(); + int unselectedColor = indicator.getUnselectedColor(); + int radiusPx = indicator.getRadius(); + int strokePx = indicator.getStroke(); + long animationDuration = indicator.getAnimationDuration(); + + BaseAnimation animation = valueController + .fill() + .with(unselectedColor, selectedColor, radiusPx, strokePx) + .duration(animationDuration); + + if (isInteractive) { + animation.progress(progress); + } else { + animation.start(); + } + + runningAnimation = animation; + } + + private void thinWormAnimation() { + int fromPosition = indicator.isInteractiveAnimation() ? indicator.getSelectedPosition() : indicator.getLastSelectedPosition(); + int toPosition = indicator.isInteractiveAnimation() ? indicator.getSelectingPosition() : indicator.getSelectedPosition(); + + int from = CoordinatesUtils.getCoordinate(indicator, fromPosition); + int to = CoordinatesUtils.getCoordinate(indicator, toPosition); + boolean isRightSide = toPosition > fromPosition; + + int radiusPx = indicator.getRadius(); + long animationDuration = indicator.getAnimationDuration(); + + BaseAnimation animation = valueController + .thinWorm() + .with(from, to, radiusPx, isRightSide) + .duration(animationDuration); + + if (isInteractive) { + animation.progress(progress); + } else { + animation.start(); + } + + runningAnimation = animation; + } + + private void dropAnimation() { + int fromPosition = indicator.isInteractiveAnimation() ? indicator.getSelectedPosition() : indicator.getLastSelectedPosition(); + int toPosition = indicator.isInteractiveAnimation() ? indicator.getSelectingPosition() : indicator.getSelectedPosition(); + + int widthFrom = CoordinatesUtils.getCoordinate(indicator, fromPosition); + int widthTo = CoordinatesUtils.getCoordinate(indicator, toPosition); + + int paddingTop = indicator.getPaddingTop(); + int paddingLeft = indicator.getPaddingLeft(); + int padding = indicator.getOrientation() == Orientation.HORIZONTAL ? paddingTop : paddingLeft; + + int radius = indicator.getRadius(); + int heightFrom = radius * 3 + padding; + int heightTo = radius + padding; + + long animationDuration = indicator.getAnimationDuration(); + + BaseAnimation animation = valueController + .drop() + .duration(animationDuration) + .with(widthFrom, widthTo, heightFrom, heightTo, radius); + + if (isInteractive) { + animation.progress(progress); + } else { + animation.start(); + } + + runningAnimation = animation; + } + + private void swapAnimation() { + int fromPosition = indicator.isInteractiveAnimation() ? indicator.getSelectedPosition() : indicator.getLastSelectedPosition(); + int toPosition = indicator.isInteractiveAnimation() ? indicator.getSelectingPosition() : indicator.getSelectedPosition(); + + int from = CoordinatesUtils.getCoordinate(indicator, fromPosition); + int to = CoordinatesUtils.getCoordinate(indicator, toPosition); + long animationDuration = indicator.getAnimationDuration(); + + BaseAnimation animation = valueController + .swap() + .with(from, to) + .duration(animationDuration); + + if (isInteractive) { + animation.progress(progress); + } else { + animation.start(); + } + + runningAnimation = animation; + } + + private void scaleDownAnimation() { + int selectedColor = indicator.getSelectedColor(); + int unselectedColor = indicator.getUnselectedColor(); + int radiusPx = indicator.getRadius(); + float scaleFactor = indicator.getScaleFactor(); + long animationDuration = indicator.getAnimationDuration(); + + BaseAnimation animation = valueController + .scaleDown() + .with(unselectedColor, selectedColor, radiusPx, scaleFactor) + .duration(animationDuration); + + if (isInteractive) { + animation.progress(progress); + } else { + animation.start(); + } + + runningAnimation = animation; + } +} + diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/animation/controller/ValueController.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/animation/controller/ValueController.java new file mode 100644 index 000000000..f3ce32804 --- /dev/null +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/animation/controller/ValueController.java @@ -0,0 +1,118 @@ +package com.smarteist.autoimageslider.IndicatorView.animation.controller; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.smarteist.autoimageslider.IndicatorView.animation.data.Value; +import com.smarteist.autoimageslider.IndicatorView.animation.type.ColorAnimation; +import com.smarteist.autoimageslider.IndicatorView.animation.type.DropAnimation; +import com.smarteist.autoimageslider.IndicatorView.animation.type.FillAnimation; +import com.smarteist.autoimageslider.IndicatorView.animation.type.ScaleAnimation; +import com.smarteist.autoimageslider.IndicatorView.animation.type.ScaleDownAnimation; +import com.smarteist.autoimageslider.IndicatorView.animation.type.SlideAnimation; +import com.smarteist.autoimageslider.IndicatorView.animation.type.SwapAnimation; +import com.smarteist.autoimageslider.IndicatorView.animation.type.ThinWormAnimation; +import com.smarteist.autoimageslider.IndicatorView.animation.type.WormAnimation; + +public class ValueController { + + private final UpdateListener updateListener; + private ColorAnimation colorAnimation; + private ScaleAnimation scaleAnimation; + private WormAnimation wormAnimation; + private SlideAnimation slideAnimation; + private FillAnimation fillAnimation; + private ThinWormAnimation thinWormAnimation; + private DropAnimation dropAnimation; + private SwapAnimation swapAnimation; + private ScaleDownAnimation scaleDownAnimation; + + public ValueController(@Nullable UpdateListener listener) { + updateListener = listener; + } + + @NonNull + public ColorAnimation color() { + if (colorAnimation == null) { + colorAnimation = new ColorAnimation(updateListener); + } + + return colorAnimation; + } + + @NonNull + public ScaleAnimation scale() { + if (scaleAnimation == null) { + scaleAnimation = new ScaleAnimation(updateListener); + } + + return scaleAnimation; + } + + @NonNull + public WormAnimation worm() { + if (wormAnimation == null) { + wormAnimation = new WormAnimation(updateListener); + } + + return wormAnimation; + } + + @NonNull + public SlideAnimation slide() { + if (slideAnimation == null) { + slideAnimation = new SlideAnimation(updateListener); + } + + return slideAnimation; + } + + @NonNull + public FillAnimation fill() { + if (fillAnimation == null) { + fillAnimation = new FillAnimation(updateListener); + } + + return fillAnimation; + } + + @NonNull + public ThinWormAnimation thinWorm() { + if (thinWormAnimation == null) { + thinWormAnimation = new ThinWormAnimation(updateListener); + } + + return thinWormAnimation; + } + + @NonNull + public DropAnimation drop() { + if (dropAnimation == null) { + dropAnimation = new DropAnimation(updateListener); + } + + return dropAnimation; + } + + @NonNull + public SwapAnimation swap() { + if (swapAnimation == null) { + swapAnimation = new SwapAnimation(updateListener); + } + + return swapAnimation; + } + + @NonNull + public ScaleDownAnimation scaleDown() { + if (scaleDownAnimation == null) { + scaleDownAnimation = new ScaleDownAnimation(updateListener); + } + + return scaleDownAnimation; + } + + public interface UpdateListener { + void onValueUpdated(@Nullable Value value); + } +} diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/animation/data/AnimationValue.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/animation/data/AnimationValue.java new file mode 100644 index 000000000..42186b8d4 --- /dev/null +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/animation/data/AnimationValue.java @@ -0,0 +1,78 @@ +package com.smarteist.autoimageslider.IndicatorView.animation.data; + +import androidx.annotation.NonNull; + +import com.smarteist.autoimageslider.IndicatorView.animation.data.type.ColorAnimationValue; +import com.smarteist.autoimageslider.IndicatorView.animation.data.type.DropAnimationValue; +import com.smarteist.autoimageslider.IndicatorView.animation.data.type.FillAnimationValue; +import com.smarteist.autoimageslider.IndicatorView.animation.data.type.ScaleAnimationValue; +import com.smarteist.autoimageslider.IndicatorView.animation.data.type.SwapAnimationValue; +import com.smarteist.autoimageslider.IndicatorView.animation.data.type.ThinWormAnimationValue; +import com.smarteist.autoimageslider.IndicatorView.animation.data.type.WormAnimationValue; + +public class AnimationValue { + + private ColorAnimationValue colorAnimationValue; + private ScaleAnimationValue scaleAnimationValue; + private WormAnimationValue wormAnimationValue; + private FillAnimationValue fillAnimationValue; + private ThinWormAnimationValue thinWormAnimationValue; + private DropAnimationValue dropAnimationValue; + private SwapAnimationValue swapAnimationValue; + + @NonNull + public ColorAnimationValue getColorAnimationValue() { + if (colorAnimationValue == null) { + colorAnimationValue = new ColorAnimationValue(); + } + return colorAnimationValue; + } + + @NonNull + public ScaleAnimationValue getScaleAnimationValue() { + if (scaleAnimationValue == null) { + scaleAnimationValue = new ScaleAnimationValue(); + } + return scaleAnimationValue; + } + + @NonNull + public WormAnimationValue getWormAnimationValue() { + if (wormAnimationValue == null) { + wormAnimationValue = new WormAnimationValue(); + } + return wormAnimationValue; + } + + @NonNull + public FillAnimationValue getFillAnimationValue() { + if (fillAnimationValue == null) { + fillAnimationValue = new FillAnimationValue(); + } + return fillAnimationValue; + } + + @NonNull + public ThinWormAnimationValue getThinWormAnimationValue() { + if (thinWormAnimationValue == null) { + thinWormAnimationValue = new ThinWormAnimationValue(); + } + return thinWormAnimationValue; + } + + @NonNull + public DropAnimationValue getDropAnimationValue() { + if (dropAnimationValue == null) { + dropAnimationValue = new DropAnimationValue(); + } + return dropAnimationValue; + } + + @NonNull + public SwapAnimationValue getSwapAnimationValue() { + if (swapAnimationValue == null) { + swapAnimationValue = new SwapAnimationValue(); + } + return swapAnimationValue; + } +} diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/animation/data/Value.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/animation/data/Value.java new file mode 100644 index 000000000..01fd56184 --- /dev/null +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/animation/data/Value.java @@ -0,0 +1,4 @@ +package com.smarteist.autoimageslider.IndicatorView.animation.data; + +public interface Value {/*empty*/ +} diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/animation/data/type/ColorAnimationValue.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/animation/data/type/ColorAnimationValue.java new file mode 100644 index 000000000..6475e85eb --- /dev/null +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/animation/data/type/ColorAnimationValue.java @@ -0,0 +1,25 @@ +package com.smarteist.autoimageslider.IndicatorView.animation.data.type; + +import com.smarteist.autoimageslider.IndicatorView.animation.data.Value; + +public class ColorAnimationValue implements Value { + + private int color; + private int colorReverse; + + public int getColor() { + return color; + } + + public void setColor(int color) { + this.color = color; + } + + public int getColorReverse() { + return colorReverse; + } + + public void setColorReverse(int colorReverse) { + this.colorReverse = colorReverse; + } +} diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/animation/data/type/DropAnimationValue.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/animation/data/type/DropAnimationValue.java new file mode 100644 index 000000000..edad95b7b --- /dev/null +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/animation/data/type/DropAnimationValue.java @@ -0,0 +1,35 @@ +package com.smarteist.autoimageslider.IndicatorView.animation.data.type; + + +import com.smarteist.autoimageslider.IndicatorView.animation.data.Value; + +public class DropAnimationValue implements Value { + + private int width; + private int height; + private int radius; + + public int getWidth() { + return width; + } + + public void setWidth(int width) { + this.width = width; + } + + public int getHeight() { + return height; + } + + public void setHeight(int height) { + this.height = height; + } + + public int getRadius() { + return radius; + } + + public void setRadius(int radius) { + this.radius = radius; + } +} diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/animation/data/type/FillAnimationValue.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/animation/data/type/FillAnimationValue.java new file mode 100644 index 000000000..be6301e07 --- /dev/null +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/animation/data/type/FillAnimationValue.java @@ -0,0 +1,44 @@ +package com.smarteist.autoimageslider.IndicatorView.animation.data.type; + +import com.smarteist.autoimageslider.IndicatorView.animation.data.Value; + +public class FillAnimationValue extends ColorAnimationValue implements Value { + + private int radius; + private int radiusReverse; + + private int stroke; + private int strokeReverse; + + public int getRadius() { + return radius; + } + + public void setRadius(int radius) { + this.radius = radius; + } + + public int getRadiusReverse() { + return radiusReverse; + } + + public void setRadiusReverse(int radiusReverse) { + this.radiusReverse = radiusReverse; + } + + public int getStroke() { + return stroke; + } + + public void setStroke(int stroke) { + this.stroke = stroke; + } + + public int getStrokeReverse() { + return strokeReverse; + } + + public void setStrokeReverse(int strokeReverse) { + this.strokeReverse = strokeReverse; + } +} diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/animation/data/type/ScaleAnimationValue.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/animation/data/type/ScaleAnimationValue.java new file mode 100644 index 000000000..697d8e2af --- /dev/null +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/animation/data/type/ScaleAnimationValue.java @@ -0,0 +1,25 @@ +package com.smarteist.autoimageslider.IndicatorView.animation.data.type; + +import com.smarteist.autoimageslider.IndicatorView.animation.data.Value; + +public class ScaleAnimationValue extends ColorAnimationValue implements Value { + + private int radius; + private int radiusReverse; + + public int getRadius() { + return radius; + } + + public void setRadius(int radius) { + this.radius = radius; + } + + public int getRadiusReverse() { + return radiusReverse; + } + + public void setRadiusReverse(int radiusReverse) { + this.radiusReverse = radiusReverse; + } +} diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/animation/data/type/SlideAnimationValue.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/animation/data/type/SlideAnimationValue.java new file mode 100644 index 000000000..d6a26065c --- /dev/null +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/animation/data/type/SlideAnimationValue.java @@ -0,0 +1,16 @@ +package com.smarteist.autoimageslider.IndicatorView.animation.data.type; + +import com.smarteist.autoimageslider.IndicatorView.animation.data.Value; + +public class SlideAnimationValue implements Value { + + private int coordinate; + + public int getCoordinate() { + return coordinate; + } + + public void setCoordinate(int coordinate) { + this.coordinate = coordinate; + } +} diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/animation/data/type/SwapAnimationValue.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/animation/data/type/SwapAnimationValue.java new file mode 100644 index 000000000..ed32499fe --- /dev/null +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/animation/data/type/SwapAnimationValue.java @@ -0,0 +1,25 @@ +package com.smarteist.autoimageslider.IndicatorView.animation.data.type; + +import com.smarteist.autoimageslider.IndicatorView.animation.data.Value; + +public class SwapAnimationValue implements Value { + + private int coordinate; + private int coordinateReverse; + + public int getCoordinate() { + return coordinate; + } + + public void setCoordinate(int coordinate) { + this.coordinate = coordinate; + } + + public int getCoordinateReverse() { + return coordinateReverse; + } + + public void setCoordinateReverse(int coordinateReverse) { + this.coordinateReverse = coordinateReverse; + } +} diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/animation/data/type/ThinWormAnimationValue.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/animation/data/type/ThinWormAnimationValue.java new file mode 100644 index 000000000..8f062e73b --- /dev/null +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/animation/data/type/ThinWormAnimationValue.java @@ -0,0 +1,16 @@ +package com.smarteist.autoimageslider.IndicatorView.animation.data.type; + +import com.smarteist.autoimageslider.IndicatorView.animation.data.Value; + +public class ThinWormAnimationValue extends WormAnimationValue implements Value { + + private int height; + + public int getHeight() { + return height; + } + + public void setHeight(int height) { + this.height = height; + } +} diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/animation/data/type/WormAnimationValue.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/animation/data/type/WormAnimationValue.java new file mode 100644 index 000000000..36a70fa8f --- /dev/null +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/animation/data/type/WormAnimationValue.java @@ -0,0 +1,25 @@ +package com.smarteist.autoimageslider.IndicatorView.animation.data.type; + +import com.smarteist.autoimageslider.IndicatorView.animation.data.Value; + +public class WormAnimationValue implements Value { + + private int rectStart; + private int rectEnd; + + public int getRectStart() { + return rectStart; + } + + public void setRectStart(int rectStartEdge) { + this.rectStart = rectStartEdge; + } + + public int getRectEnd() { + return rectEnd; + } + + public void setRectEnd(int rectEnd) { + this.rectEnd = rectEnd; + } +} diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/animation/type/BaseAnimation.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/animation/type/BaseAnimation.java new file mode 100644 index 000000000..e525c3f72 --- /dev/null +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/animation/type/BaseAnimation.java @@ -0,0 +1,50 @@ +package com.smarteist.autoimageslider.IndicatorView.animation.type; + +import android.animation.Animator; +import android.animation.ValueAnimator; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.smarteist.autoimageslider.IndicatorView.animation.controller.ValueController; + +public abstract class BaseAnimation { + + public static final int DEFAULT_ANIMATION_TIME = 350; + protected long animationDuration = DEFAULT_ANIMATION_TIME; + + protected ValueController.UpdateListener listener; + protected T animator; + + public BaseAnimation(@Nullable ValueController.UpdateListener listener) { + this.listener = listener; + animator = createAnimator(); + } + + @NonNull + public abstract T createAnimator(); + + public abstract BaseAnimation progress(float progress); + + public BaseAnimation duration(long duration) { + animationDuration = duration; + + if (animator instanceof ValueAnimator) { + animator.setDuration(animationDuration); + } + + return this; + } + + public void start() { + if (animator != null && !animator.isRunning()) { + animator.start(); + } + } + + public void end() { + if (animator != null && animator.isStarted()) { + animator.end(); + } + } +} diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/animation/type/ColorAnimation.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/animation/type/ColorAnimation.java new file mode 100644 index 000000000..91bae136f --- /dev/null +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/animation/type/ColorAnimation.java @@ -0,0 +1,123 @@ +package com.smarteist.autoimageslider.IndicatorView.animation.type; + +import android.animation.ArgbEvaluator; +import android.animation.PropertyValuesHolder; +import android.animation.ValueAnimator; +import android.view.animation.AccelerateDecelerateInterpolator; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.smarteist.autoimageslider.IndicatorView.animation.controller.ValueController; +import com.smarteist.autoimageslider.IndicatorView.animation.data.type.ColorAnimationValue; + +public class ColorAnimation extends BaseAnimation { + + public static final String DEFAULT_UNSELECTED_COLOR = "#33ffffff"; + public static final String DEFAULT_SELECTED_COLOR = "#ffffff"; + + static final String ANIMATION_COLOR_REVERSE = "ANIMATION_COLOR_REVERSE"; + static final String ANIMATION_COLOR = "ANIMATION_COLOR"; + + private final ColorAnimationValue value; + + int colorStart; + int colorEnd; + + public ColorAnimation(@Nullable ValueController.UpdateListener listener) { + super(listener); + value = new ColorAnimationValue(); + } + + @NonNull + @Override + public ValueAnimator createAnimator() { + ValueAnimator animator = new ValueAnimator(); + animator.setDuration(BaseAnimation.DEFAULT_ANIMATION_TIME); + animator.setInterpolator(new AccelerateDecelerateInterpolator()); + animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + onAnimateUpdated(animation); + } + }); + + return animator; + } + + @Override + public ColorAnimation progress(float progress) { + if (animator != null) { + long playTime = (long) (progress * animationDuration); + + if (animator.getValues() != null && animator.getValues().length > 0) { + animator.setCurrentPlayTime(playTime); + } + } + + return this; + } + + @NonNull + public ColorAnimation with(int colorStart, int colorEnd) { + if (animator != null && hasChanges(colorStart, colorEnd)) { + + this.colorStart = colorStart; + this.colorEnd = colorEnd; + + PropertyValuesHolder colorHolder = createColorPropertyHolder(false); + PropertyValuesHolder reverseColorHolder = createColorPropertyHolder(true); + + animator.setValues(colorHolder, reverseColorHolder); + } + + return this; + } + + PropertyValuesHolder createColorPropertyHolder(boolean isReverse) { + String propertyName; + int colorStart; + int colorEnd; + + if (isReverse) { + propertyName = ANIMATION_COLOR_REVERSE; + colorStart = this.colorEnd; + colorEnd = this.colorStart; + + } else { + propertyName = ANIMATION_COLOR; + colorStart = this.colorStart; + colorEnd = this.colorEnd; + } + + PropertyValuesHolder holder = PropertyValuesHolder.ofInt(propertyName, colorStart, colorEnd); + holder.setEvaluator(new ArgbEvaluator()); + + return holder; + } + + @SuppressWarnings("RedundantIfStatement") + private boolean hasChanges(int colorStart, int colorEnd) { + if (this.colorStart != colorStart) { + return true; + } + + if (this.colorEnd != colorEnd) { + return true; + } + + return false; + } + + private void onAnimateUpdated(@NonNull ValueAnimator animation) { + int color = (int) animation.getAnimatedValue(ANIMATION_COLOR); + int colorReverse = (int) animation.getAnimatedValue(ANIMATION_COLOR_REVERSE); + + value.setColor(color); + value.setColorReverse(colorReverse); + + if (listener != null) { + listener.onValueUpdated(value); + } + } +} diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/animation/type/DropAnimation.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/animation/type/DropAnimation.java new file mode 100644 index 000000000..c9b88e02c --- /dev/null +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/animation/type/DropAnimation.java @@ -0,0 +1,172 @@ +package com.smarteist.autoimageslider.IndicatorView.animation.type; + +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.animation.ValueAnimator; +import android.view.animation.AccelerateDecelerateInterpolator; + +import androidx.annotation.NonNull; + +import com.smarteist.autoimageslider.IndicatorView.animation.controller.ValueController; +import com.smarteist.autoimageslider.IndicatorView.animation.data.type.DropAnimationValue; + +public class DropAnimation extends BaseAnimation { + + private final DropAnimationValue value; + private int widthStart; + private int widthEnd; + private int heightStart; + private int heightEnd; + private int radius; + + public DropAnimation(@NonNull ValueController.UpdateListener listener) { + super(listener); + value = new DropAnimationValue(); + } + + @NonNull + @Override + public AnimatorSet createAnimator() { + AnimatorSet animator = new AnimatorSet(); + animator.setInterpolator(new AccelerateDecelerateInterpolator()); + + return animator; + } + + @Override + public DropAnimation progress(float progress) { + if (animator != null) { + long playTimeLeft = (long) (progress * animationDuration); + boolean isReverse = false; + + for (Animator anim : animator.getChildAnimations()) { + ValueAnimator animator = (ValueAnimator) anim; + long animDuration = animator.getDuration(); + long currPlayTime = playTimeLeft; + + if (isReverse) { + currPlayTime -= animDuration; + } + + if (currPlayTime < 0) { + continue; + + } else if (currPlayTime >= animDuration) { + currPlayTime = animDuration; + } + + if (animator.getValues() != null && animator.getValues().length > 0) { + animator.setCurrentPlayTime(currPlayTime); + } + + if (!isReverse && animDuration >= animationDuration) { + isReverse = true; + } + } + } + + return this; + } + + @Override + public DropAnimation duration(long duration) { + super.duration(duration); + return this; + } + + @SuppressWarnings("UnnecessaryLocalVariable") + public DropAnimation with(int widthStart, int widthEnd, int heightStart, int heightEnd, int radius) { + if (hasChanges(widthStart, widthEnd, heightStart, heightEnd, radius)) { + animator = createAnimator(); + + this.widthStart = widthStart; + this.widthEnd = widthEnd; + this.heightStart = heightStart; + this.heightEnd = heightEnd; + this.radius = radius; + + int fromRadius = radius; + int toRadius = (int) (radius / 1.5); + long halfDuration = animationDuration / 2; + + ValueAnimator widthAnimator = createValueAnimation(widthStart, widthEnd, animationDuration, AnimationType.Width); + ValueAnimator heightForwardAnimator = createValueAnimation(heightStart, heightEnd, halfDuration, AnimationType.Height); + ValueAnimator radiusForwardAnimator = createValueAnimation(fromRadius, toRadius, halfDuration, AnimationType.Radius); + + ValueAnimator heightBackwardAnimator = createValueAnimation(heightEnd, heightStart, halfDuration, AnimationType.Height); + ValueAnimator radiusBackwardAnimator = createValueAnimation(toRadius, fromRadius, halfDuration, AnimationType.Radius); + + animator.play(heightForwardAnimator) + .with(radiusForwardAnimator) + .with(widthAnimator) + .before(heightBackwardAnimator) + .before(radiusBackwardAnimator); + } + + return this; + } + + private ValueAnimator createValueAnimation(int fromValue, int toValue, long duration, final AnimationType type) { + ValueAnimator anim = ValueAnimator.ofInt(fromValue, toValue); + anim.setInterpolator(new AccelerateDecelerateInterpolator()); + anim.setDuration(duration); + anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + onAnimatorUpdate(animation, type); + } + }); + + return anim; + } + + private void onAnimatorUpdate(@NonNull ValueAnimator animation, @NonNull AnimationType type) { + int frameValue = (int) animation.getAnimatedValue(); + + switch (type) { + case Width: + value.setWidth(frameValue); + break; + + case Height: + value.setHeight(frameValue); + break; + + case Radius: + value.setRadius(frameValue); + break; + } + + if (listener != null) { + listener.onValueUpdated(value); + } + } + + @SuppressWarnings("RedundantIfStatement") + private boolean hasChanges(int widthStart, int widthEnd, int heightStart, int heightEnd, int radius) { + if (this.widthStart != widthStart) { + return true; + } + + if (this.widthEnd != widthEnd) { + return true; + } + + if (this.heightStart != heightStart) { + return true; + } + + if (this.heightEnd != heightEnd) { + return true; + } + + if (this.radius != radius) { + return true; + } + + return false; + } + + private enum AnimationType {Width, Height, Radius} + +} diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/animation/type/FillAnimation.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/animation/type/FillAnimation.java new file mode 100644 index 000000000..476ba1ab5 --- /dev/null +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/animation/type/FillAnimation.java @@ -0,0 +1,167 @@ +package com.smarteist.autoimageslider.IndicatorView.animation.type; + +import android.animation.IntEvaluator; +import android.animation.PropertyValuesHolder; +import android.animation.ValueAnimator; +import android.view.animation.AccelerateDecelerateInterpolator; + +import androidx.annotation.NonNull; + +import com.smarteist.autoimageslider.IndicatorView.animation.controller.ValueController; +import com.smarteist.autoimageslider.IndicatorView.animation.data.type.FillAnimationValue; + +public class FillAnimation extends ColorAnimation { + + public static final int DEFAULT_STROKE_DP = 1; + private static final String ANIMATION_RADIUS_REVERSE = "ANIMATION_RADIUS_REVERSE"; + private static final String ANIMATION_RADIUS = "ANIMATION_RADIUS"; + private static final String ANIMATION_STROKE_REVERSE = "ANIMATION_STROKE_REVERSE"; + private static final String ANIMATION_STROKE = "ANIMATION_STROKE"; + private final FillAnimationValue value; + + private int radius; + private int stroke; + + public FillAnimation(@NonNull ValueController.UpdateListener listener) { + super(listener); + value = new FillAnimationValue(); + } + + @NonNull + @Override + public ValueAnimator createAnimator() { + ValueAnimator animator = new ValueAnimator(); + animator.setDuration(BaseAnimation.DEFAULT_ANIMATION_TIME); + animator.setInterpolator(new AccelerateDecelerateInterpolator()); + animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + onAnimateUpdated(animation); + } + }); + + return animator; + } + + @NonNull + public FillAnimation with(int colorStart, int colorEnd, int radius, int stroke) { + if (animator != null && hasChanges(colorStart, colorEnd, radius, stroke)) { + + this.colorStart = colorStart; + this.colorEnd = colorEnd; + + this.radius = radius; + this.stroke = stroke; + + PropertyValuesHolder colorHolder = createColorPropertyHolder(false); + PropertyValuesHolder reverseColorHolder = createColorPropertyHolder(true); + + PropertyValuesHolder radiusHolder = createRadiusPropertyHolder(false); + PropertyValuesHolder radiusReverseHolder = createRadiusPropertyHolder(true); + + PropertyValuesHolder strokeHolder = createStrokePropertyHolder(false); + PropertyValuesHolder strokeReverseHolder = createStrokePropertyHolder(true); + + animator.setValues( + colorHolder, + reverseColorHolder, + + radiusHolder, + radiusReverseHolder, + + strokeHolder, + strokeReverseHolder); + } + + return this; + } + + @NonNull + private PropertyValuesHolder createRadiusPropertyHolder(boolean isReverse) { + String propertyName; + int startRadiusValue; + int endRadiusValue; + + if (isReverse) { + propertyName = ANIMATION_RADIUS_REVERSE; + startRadiusValue = radius / 2; + endRadiusValue = radius; + } else { + propertyName = ANIMATION_RADIUS; + startRadiusValue = radius; + endRadiusValue = radius / 2; + } + + PropertyValuesHolder holder = PropertyValuesHolder.ofInt(propertyName, startRadiusValue, endRadiusValue); + holder.setEvaluator(new IntEvaluator()); + + return holder; + } + + @NonNull + private PropertyValuesHolder createStrokePropertyHolder(boolean isReverse) { + String propertyName; + int startStrokeValue; + int endStrokeValue; + + if (isReverse) { + propertyName = ANIMATION_STROKE_REVERSE; + startStrokeValue = radius; + endStrokeValue = 0; + } else { + propertyName = ANIMATION_STROKE; + startStrokeValue = 0; + endStrokeValue = radius; + } + + PropertyValuesHolder holder = PropertyValuesHolder.ofInt(propertyName, startStrokeValue, endStrokeValue); + holder.setEvaluator(new IntEvaluator()); + + return holder; + } + + private void onAnimateUpdated(@NonNull ValueAnimator animation) { + int color = (int) animation.getAnimatedValue(ANIMATION_COLOR); + int colorReverse = (int) animation.getAnimatedValue(ANIMATION_COLOR_REVERSE); + + int radius = (int) animation.getAnimatedValue(ANIMATION_RADIUS); + int radiusReverse = (int) animation.getAnimatedValue(ANIMATION_RADIUS_REVERSE); + + int stroke = (int) animation.getAnimatedValue(ANIMATION_STROKE); + int strokeReverse = (int) animation.getAnimatedValue(ANIMATION_STROKE_REVERSE); + + value.setColor(color); + value.setColorReverse(colorReverse); + + value.setRadius(radius); + value.setRadiusReverse(radiusReverse); + + value.setStroke(stroke); + value.setStrokeReverse(strokeReverse); + + if (listener != null) { + listener.onValueUpdated(value); + } + } + + @SuppressWarnings("RedundantIfStatement") + private boolean hasChanges(int colorStart, int colorEnd, int radiusValue, int strokeValue) { + if (this.colorStart != colorStart) { + return true; + } + + if (this.colorEnd != colorEnd) { + return true; + } + + if (radius != radiusValue) { + return true; + } + + if (stroke != strokeValue) { + return true; + } + + return false; + } +} diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/animation/type/IndicatorAnimationType.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/animation/type/IndicatorAnimationType.java new file mode 100644 index 000000000..254bf75f2 --- /dev/null +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/animation/type/IndicatorAnimationType.java @@ -0,0 +1,3 @@ +package com.smarteist.autoimageslider.IndicatorView.animation.type; + +public enum IndicatorAnimationType {NONE, COLOR, SCALE, WORM, SLIDE, FILL, THIN_WORM, DROP, SWAP, SCALE_DOWN} diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/animation/type/ScaleAnimation.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/animation/type/ScaleAnimation.java new file mode 100644 index 000000000..04ba39ace --- /dev/null +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/animation/type/ScaleAnimation.java @@ -0,0 +1,129 @@ +package com.smarteist.autoimageslider.IndicatorView.animation.type; + +import android.animation.IntEvaluator; +import android.animation.PropertyValuesHolder; +import android.animation.ValueAnimator; +import android.view.animation.AccelerateDecelerateInterpolator; + +import androidx.annotation.NonNull; + +import com.smarteist.autoimageslider.IndicatorView.animation.controller.ValueController; +import com.smarteist.autoimageslider.IndicatorView.animation.data.type.ScaleAnimationValue; + +public class ScaleAnimation extends ColorAnimation { + + public static final float DEFAULT_SCALE_FACTOR = 0.7f; + public static final float MIN_SCALE_FACTOR = 0.3f; + public static final float MAX_SCALE_FACTOR = 1; + + static final String ANIMATION_SCALE_REVERSE = "ANIMATION_SCALE_REVERSE"; + static final String ANIMATION_SCALE = "ANIMATION_SCALE"; + private final ScaleAnimationValue value; + int radius; + float scaleFactor; + + public ScaleAnimation(@NonNull ValueController.UpdateListener listener) { + super(listener); + value = new ScaleAnimationValue(); + } + + @NonNull + @Override + public ValueAnimator createAnimator() { + ValueAnimator animator = new ValueAnimator(); + animator.setDuration(BaseAnimation.DEFAULT_ANIMATION_TIME); + animator.setInterpolator(new AccelerateDecelerateInterpolator()); + animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + onAnimateUpdated(animation); + } + }); + + return animator; + } + + @NonNull + public ScaleAnimation with(int colorStart, int colorEnd, int radius, float scaleFactor) { + if (animator != null && hasChanges(colorStart, colorEnd, radius, scaleFactor)) { + + this.colorStart = colorStart; + this.colorEnd = colorEnd; + + this.radius = radius; + this.scaleFactor = scaleFactor; + + PropertyValuesHolder colorHolder = createColorPropertyHolder(false); + PropertyValuesHolder reverseColorHolder = createColorPropertyHolder(true); + + PropertyValuesHolder scaleHolder = createScalePropertyHolder(false); + PropertyValuesHolder scaleReverseHolder = createScalePropertyHolder(true); + + animator.setValues(colorHolder, reverseColorHolder, scaleHolder, scaleReverseHolder); + } + + return this; + } + + private void onAnimateUpdated(@NonNull ValueAnimator animation) { + int color = (int) animation.getAnimatedValue(ANIMATION_COLOR); + int colorReverse = (int) animation.getAnimatedValue(ANIMATION_COLOR_REVERSE); + + int radius = (int) animation.getAnimatedValue(ANIMATION_SCALE); + int radiusReverse = (int) animation.getAnimatedValue(ANIMATION_SCALE_REVERSE); + + value.setColor(color); + value.setColorReverse(colorReverse); + + value.setRadius(radius); + value.setRadiusReverse(radiusReverse); + + if (listener != null) { + listener.onValueUpdated(value); + } + } + + @NonNull + protected PropertyValuesHolder createScalePropertyHolder(boolean isReverse) { + String propertyName; + int startRadiusValue; + int endRadiusValue; + + if (isReverse) { + propertyName = ANIMATION_SCALE_REVERSE; + startRadiusValue = radius; + endRadiusValue = (int) (radius * scaleFactor); + } else { + propertyName = ANIMATION_SCALE; + startRadiusValue = (int) (radius * scaleFactor); + endRadiusValue = radius; + } + + PropertyValuesHolder holder = PropertyValuesHolder.ofInt(propertyName, startRadiusValue, endRadiusValue); + holder.setEvaluator(new IntEvaluator()); + + return holder; + } + + @SuppressWarnings("RedundantIfStatement") + private boolean hasChanges(int colorStart, int colorEnd, int radiusValue, float scaleFactorValue) { + if (this.colorStart != colorStart) { + return true; + } + + if (this.colorEnd != colorEnd) { + return true; + } + + if (radius != radiusValue) { + return true; + } + + if (scaleFactor != scaleFactorValue) { + return true; + } + + return false; + } +} + diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/animation/type/ScaleDownAnimation.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/animation/type/ScaleDownAnimation.java new file mode 100644 index 000000000..6a52eaf43 --- /dev/null +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/animation/type/ScaleDownAnimation.java @@ -0,0 +1,39 @@ +package com.smarteist.autoimageslider.IndicatorView.animation.type; + +import android.animation.IntEvaluator; +import android.animation.PropertyValuesHolder; + +import androidx.annotation.NonNull; + +import com.smarteist.autoimageslider.IndicatorView.animation.controller.ValueController; + +public class ScaleDownAnimation extends ScaleAnimation { + + public ScaleDownAnimation(@NonNull ValueController.UpdateListener listener) { + super(listener); + } + + @NonNull + @Override + protected PropertyValuesHolder createScalePropertyHolder(boolean isReverse) { + String propertyName; + int startRadiusValue; + int endRadiusValue; + + if (isReverse) { + propertyName = ANIMATION_SCALE_REVERSE; + startRadiusValue = (int) (radius * scaleFactor); + endRadiusValue = radius; + } else { + propertyName = ANIMATION_SCALE; + startRadiusValue = radius; + endRadiusValue = (int) (radius * scaleFactor); + } + + PropertyValuesHolder holder = PropertyValuesHolder.ofInt(propertyName, startRadiusValue, endRadiusValue); + holder.setEvaluator(new IntEvaluator()); + + return holder; + } +} + diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/animation/type/SlideAnimation.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/animation/type/SlideAnimation.java new file mode 100644 index 000000000..ecad67f32 --- /dev/null +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/animation/type/SlideAnimation.java @@ -0,0 +1,98 @@ +package com.smarteist.autoimageslider.IndicatorView.animation.type; + +import android.animation.IntEvaluator; +import android.animation.PropertyValuesHolder; +import android.animation.ValueAnimator; +import android.view.animation.AccelerateDecelerateInterpolator; + +import androidx.annotation.NonNull; + +import com.smarteist.autoimageslider.IndicatorView.animation.controller.ValueController; +import com.smarteist.autoimageslider.IndicatorView.animation.data.type.SlideAnimationValue; + +public class SlideAnimation extends BaseAnimation { + + private static final String ANIMATION_COORDINATE = "ANIMATION_COORDINATE"; + private static final int COORDINATE_NONE = -1; + + private final SlideAnimationValue value; + private int coordinateStart = COORDINATE_NONE; + private int coordinateEnd = COORDINATE_NONE; + + public SlideAnimation(@NonNull ValueController.UpdateListener listener) { + super(listener); + value = new SlideAnimationValue(); + } + + @NonNull + @Override + public ValueAnimator createAnimator() { + ValueAnimator animator = new ValueAnimator(); + animator.setDuration(BaseAnimation.DEFAULT_ANIMATION_TIME); + animator.setInterpolator(new AccelerateDecelerateInterpolator()); + animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + onAnimateUpdated(animation); + } + }); + + return animator; + } + + @Override + public SlideAnimation progress(float progress) { + if (animator != null) { + long playTime = (long) (progress * animationDuration); + + if (animator.getValues() != null && animator.getValues().length > 0) { + animator.setCurrentPlayTime(playTime); + } + } + + return this; + } + + @NonNull + public SlideAnimation with(int coordinateStart, int coordinateEnd) { + if (animator != null && hasChanges(coordinateStart, coordinateEnd)) { + + this.coordinateStart = coordinateStart; + this.coordinateEnd = coordinateEnd; + + PropertyValuesHolder holder = createSlidePropertyHolder(); + animator.setValues(holder); + } + + return this; + } + + private PropertyValuesHolder createSlidePropertyHolder() { + PropertyValuesHolder holder = PropertyValuesHolder.ofInt(ANIMATION_COORDINATE, coordinateStart, coordinateEnd); + holder.setEvaluator(new IntEvaluator()); + + return holder; + } + + private void onAnimateUpdated(@NonNull ValueAnimator animation) { + int coordinate = (int) animation.getAnimatedValue(ANIMATION_COORDINATE); + value.setCoordinate(coordinate); + + if (listener != null) { + listener.onValueUpdated(value); + } + } + + @SuppressWarnings("RedundantIfStatement") + private boolean hasChanges(int coordinateStart, int coordinateEnd) { + if (this.coordinateStart != coordinateStart) { + return true; + } + + if (this.coordinateEnd != coordinateEnd) { + return true; + } + + return false; + } +} diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/animation/type/SwapAnimation.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/animation/type/SwapAnimation.java new file mode 100644 index 000000000..af5f92279 --- /dev/null +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/animation/type/SwapAnimation.java @@ -0,0 +1,101 @@ +package com.smarteist.autoimageslider.IndicatorView.animation.type; + +import android.animation.IntEvaluator; +import android.animation.PropertyValuesHolder; +import android.animation.ValueAnimator; +import android.view.animation.AccelerateDecelerateInterpolator; + +import androidx.annotation.NonNull; + +import com.smarteist.autoimageslider.IndicatorView.animation.controller.ValueController; +import com.smarteist.autoimageslider.IndicatorView.animation.data.type.SwapAnimationValue; + +public class SwapAnimation extends BaseAnimation { + + private static final String ANIMATION_COORDINATE = "ANIMATION_COORDINATE"; + private static final String ANIMATION_COORDINATE_REVERSE = "ANIMATION_COORDINATE_REVERSE"; + private static final int COORDINATE_NONE = -1; + private final SwapAnimationValue value; + private int coordinateStart = COORDINATE_NONE; + private int coordinateEnd = COORDINATE_NONE; + + public SwapAnimation(@NonNull ValueController.UpdateListener listener) { + super(listener); + value = new SwapAnimationValue(); + } + + @NonNull + @Override + public ValueAnimator createAnimator() { + ValueAnimator animator = new ValueAnimator(); + animator.setDuration(BaseAnimation.DEFAULT_ANIMATION_TIME); + animator.setInterpolator(new AccelerateDecelerateInterpolator()); + animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + onAnimateUpdated(animation); + } + }); + + return animator; + } + + @Override + public SwapAnimation progress(float progress) { + if (animator != null) { + long playTime = (long) (progress * animationDuration); + + if (animator.getValues() != null && animator.getValues().length > 0) { + animator.setCurrentPlayTime(playTime); + } + } + + return this; + } + + @NonNull + public SwapAnimation with(int coordinateStart, int coordinateEnd) { + if (animator != null && hasChanges(coordinateStart, coordinateEnd)) { + this.coordinateStart = coordinateStart; + this.coordinateEnd = coordinateEnd; + + PropertyValuesHolder holder = createColorPropertyHolder(ANIMATION_COORDINATE, coordinateStart, coordinateEnd); + PropertyValuesHolder holderReverse = createColorPropertyHolder(ANIMATION_COORDINATE_REVERSE, coordinateEnd, coordinateStart); + animator.setValues(holder, holderReverse); + } + + return this; + } + + private PropertyValuesHolder createColorPropertyHolder(String propertyName, int startValue, int endValue) { + PropertyValuesHolder holder = PropertyValuesHolder.ofInt(propertyName, startValue, endValue); + holder.setEvaluator(new IntEvaluator()); + + return holder; + } + + private void onAnimateUpdated(@NonNull ValueAnimator animation) { + int coordinate = (int) animation.getAnimatedValue(ANIMATION_COORDINATE); + int coordinateReverse = (int) animation.getAnimatedValue(ANIMATION_COORDINATE_REVERSE); + + value.setCoordinate(coordinate); + value.setCoordinateReverse(coordinateReverse); + + if (listener != null) { + listener.onValueUpdated(value); + } + } + + @SuppressWarnings("RedundantIfStatement") + private boolean hasChanges(int coordinateStart, int coordinateEnd) { + if (this.coordinateStart != coordinateStart) { + return true; + } + + if (this.coordinateEnd != coordinateEnd) { + return true; + } + + return false; + } +} diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/animation/type/ThinWormAnimation.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/animation/type/ThinWormAnimation.java new file mode 100644 index 000000000..2b2f6080d --- /dev/null +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/animation/type/ThinWormAnimation.java @@ -0,0 +1,118 @@ +package com.smarteist.autoimageslider.IndicatorView.animation.type; + +import android.animation.ValueAnimator; +import android.view.animation.AccelerateDecelerateInterpolator; + +import androidx.annotation.NonNull; + +import com.smarteist.autoimageslider.IndicatorView.animation.controller.ValueController; +import com.smarteist.autoimageslider.IndicatorView.animation.data.type.ThinWormAnimationValue; + +public class ThinWormAnimation extends WormAnimation { + + private final ThinWormAnimationValue value; + + public ThinWormAnimation(@NonNull ValueController.UpdateListener listener) { + super(listener); + value = new ThinWormAnimationValue(); + } + + @Override + public ThinWormAnimation duration(long duration) { + super.duration(duration); + return this; + } + + @Override + public WormAnimation with(int coordinateStart, int coordinateEnd, int radius, boolean isRightSide) { + if (hasChanges(coordinateStart, coordinateEnd, radius, isRightSide)) { + animator = createAnimator(); + + this.coordinateStart = coordinateStart; + this.coordinateEnd = coordinateEnd; + + this.radius = radius; + this.isRightSide = isRightSide; + + int height = radius * 2; + rectLeftEdge = coordinateStart - radius; + rectRightEdge = coordinateStart + radius; + + value.setRectStart(rectLeftEdge); + value.setRectEnd(rectRightEdge); + value.setHeight(height); + + RectValues rec = createRectValues(isRightSide); + long sizeDuration = (long) (animationDuration * 0.8); + long reverseDelay = (long) (animationDuration * 0.2); + + long heightDuration = (long) (animationDuration * 0.5); + long reverseHeightDelay = (long) (animationDuration * 0.5); + + ValueAnimator straightAnimator = createWormAnimator(rec.fromX, rec.toX, sizeDuration, false, value); + ValueAnimator reverseAnimator = createWormAnimator(rec.reverseFromX, rec.reverseToX, sizeDuration, true, value); + reverseAnimator.setStartDelay(reverseDelay); + + ValueAnimator straightHeightAnimator = createHeightAnimator(height, radius, heightDuration); + ValueAnimator reverseHeightAnimator = createHeightAnimator(radius, height, heightDuration); + reverseHeightAnimator.setStartDelay(reverseHeightDelay); + + animator.playTogether(straightAnimator, reverseAnimator, straightHeightAnimator, reverseHeightAnimator); + } + return this; + } + + private ValueAnimator createHeightAnimator(int fromHeight, int toHeight, long duration) { + ValueAnimator anim = ValueAnimator.ofInt(fromHeight, toHeight); + anim.setInterpolator(new AccelerateDecelerateInterpolator()); + anim.setDuration(duration); + anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + onAnimateUpdated(animation); + } + }); + + return anim; + } + + private void onAnimateUpdated(@NonNull ValueAnimator animation) { + value.setHeight((int) animation.getAnimatedValue()); + + if (listener != null) { + listener.onValueUpdated(value); + } + } + + @Override + public ThinWormAnimation progress(float progress) { + if (animator != null) { + long progressDuration = (long) (progress * animationDuration); + int size = animator.getChildAnimations().size(); + + for (int i = 0; i < size; i++) { + ValueAnimator anim = (ValueAnimator) animator.getChildAnimations().get(i); + + long setDuration = progressDuration - anim.getStartDelay(); + long duration = anim.getDuration(); + + if (setDuration > duration) { + setDuration = duration; + + } else if (setDuration < 0) { + setDuration = 0; + } + + if (i == size - 1 && setDuration <= 0) { + continue; + } + + if (anim.getValues() != null && anim.getValues().length > 0) { + anim.setCurrentPlayTime(setDuration); + } + } + } + + return this; + } +} \ No newline at end of file diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/animation/type/WormAnimation.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/animation/type/WormAnimation.java new file mode 100644 index 000000000..4a3f5b2fb --- /dev/null +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/animation/type/WormAnimation.java @@ -0,0 +1,198 @@ +package com.smarteist.autoimageslider.IndicatorView.animation.type; + +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.animation.ValueAnimator; +import android.view.animation.AccelerateDecelerateInterpolator; + +import androidx.annotation.NonNull; + +import com.smarteist.autoimageslider.IndicatorView.animation.controller.ValueController; +import com.smarteist.autoimageslider.IndicatorView.animation.data.type.WormAnimationValue; + +public class WormAnimation extends BaseAnimation { + + private final WormAnimationValue value; + int coordinateStart; + int coordinateEnd; + int radius; + boolean isRightSide; + int rectLeftEdge; + int rectRightEdge; + + public WormAnimation(@NonNull ValueController.UpdateListener listener) { + super(listener); + value = new WormAnimationValue(); + } + + @NonNull + @Override + public AnimatorSet createAnimator() { + AnimatorSet animator = new AnimatorSet(); + animator.setInterpolator(new AccelerateDecelerateInterpolator()); + + return animator; + } + + @Override + public WormAnimation duration(long duration) { + super.duration(duration); + return this; + } + + public WormAnimation with(int coordinateStart, int coordinateEnd, int radius, boolean isRightSide) { + if (hasChanges(coordinateStart, coordinateEnd, radius, isRightSide)) { + animator = createAnimator(); + + this.coordinateStart = coordinateStart; + this.coordinateEnd = coordinateEnd; + + this.radius = radius; + this.isRightSide = isRightSide; + + rectLeftEdge = coordinateStart - radius; + rectRightEdge = coordinateStart + radius; + + value.setRectStart(rectLeftEdge); + value.setRectEnd(rectRightEdge); + + RectValues rect = createRectValues(isRightSide); + long duration = animationDuration / 2; + + ValueAnimator straightAnimator = createWormAnimator(rect.fromX, rect.toX, duration, false, value); + ValueAnimator reverseAnimator = createWormAnimator(rect.reverseFromX, rect.reverseToX, duration, true, value); + animator.playSequentially(straightAnimator, reverseAnimator); + } + return this; + } + + @Override + public WormAnimation progress(float progress) { + if (animator == null) { + return this; + } + + long progressDuration = (long) (progress * animationDuration); + for (Animator anim : animator.getChildAnimations()) { + ValueAnimator animator = (ValueAnimator) anim; + long duration = animator.getDuration(); + long setDuration = progressDuration; + + if (setDuration > duration) { + setDuration = duration; + } + + animator.setCurrentPlayTime(setDuration); + progressDuration -= setDuration; + } + + return this; + } + + ValueAnimator createWormAnimator( + int fromValue, + int toValue, + long duration, + final boolean isReverse, + final WormAnimationValue value) { + + ValueAnimator anim = ValueAnimator.ofInt(fromValue, toValue); + anim.setInterpolator(new AccelerateDecelerateInterpolator()); + anim.setDuration(duration); + anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + onAnimateUpdated(value, animation, isReverse); + } + }); + + return anim; + } + + private void onAnimateUpdated(@NonNull WormAnimationValue value, @NonNull ValueAnimator animation, final boolean isReverse) { + int rectEdge = (int) animation.getAnimatedValue(); + + if (isRightSide) { + if (!isReverse) { + value.setRectEnd(rectEdge); + } else { + value.setRectStart(rectEdge); + } + + } else { + if (!isReverse) { + value.setRectStart(rectEdge); + } else { + value.setRectEnd(rectEdge); + } + } + + if (listener != null) { + listener.onValueUpdated(value); + } + } + + @SuppressWarnings("RedundantIfStatement") + boolean hasChanges(int coordinateStart, int coordinateEnd, int radius, boolean isRightSide) { + if (this.coordinateStart != coordinateStart) { + return true; + } + + if (this.coordinateEnd != coordinateEnd) { + return true; + } + + if (this.radius != radius) { + return true; + } + + if (this.isRightSide != isRightSide) { + return true; + } + + return false; + } + + @NonNull + RectValues createRectValues(boolean isRightSide) { + int fromX; + int toX; + + int reverseFromX; + int reverseToX; + + if (isRightSide) { + fromX = coordinateStart + radius; + toX = coordinateEnd + radius; + + reverseFromX = coordinateStart - radius; + reverseToX = coordinateEnd - radius; + + } else { + fromX = coordinateStart - radius; + toX = coordinateEnd - radius; + + reverseFromX = coordinateStart + radius; + reverseToX = coordinateEnd + radius; + } + + return new RectValues(fromX, toX, reverseFromX, reverseToX); + } + + class RectValues { + + final int fromX; + final int toX; + + final int reverseFromX; + final int reverseToX; + + RectValues(int fromX, int toX, int reverseFromX, int reverseToX) { + this.fromX = fromX; + this.toX = toX; + + this.reverseFromX = reverseFromX; + this.reverseToX = reverseToX; + } + } +} diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/draw/DrawManager.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/draw/DrawManager.java new file mode 100644 index 000000000..b9764fa2e --- /dev/null +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/draw/DrawManager.java @@ -0,0 +1,64 @@ +package com.smarteist.autoimageslider.IndicatorView.draw; + +import android.content.Context; +import android.graphics.Canvas; +import android.util.AttributeSet; +import android.util.Pair; +import android.view.MotionEvent; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.smarteist.autoimageslider.IndicatorView.animation.data.Value; +import com.smarteist.autoimageslider.IndicatorView.draw.controller.AttributeController; +import com.smarteist.autoimageslider.IndicatorView.draw.controller.DrawController; +import com.smarteist.autoimageslider.IndicatorView.draw.controller.MeasureController; +import com.smarteist.autoimageslider.IndicatorView.draw.data.Indicator; + +public class DrawManager { + + private final DrawController drawController; + private final MeasureController measureController; + private final AttributeController attributeController; + private Indicator indicator; + + public DrawManager() { + this.indicator = new Indicator(); + this.drawController = new DrawController(indicator); + this.measureController = new MeasureController(); + this.attributeController = new AttributeController(indicator); + } + + @NonNull + public Indicator indicator() { + if (indicator == null) { + indicator = new Indicator(); + } + + return indicator; + } + + public void setClickListener(@Nullable DrawController.ClickListener listener) { + drawController.setClickListener(listener); + } + + public void touch(@Nullable MotionEvent event) { + drawController.touch(event); + } + + public void updateValue(@Nullable Value value) { + drawController.updateValue(value); + } + + public void draw(@NonNull Canvas canvas) { + drawController.draw(canvas); + } + + public Pair measureViewSize(int widthMeasureSpec, int heightMeasureSpec) { + return measureController.measureViewSize(indicator, widthMeasureSpec, heightMeasureSpec); + } + + public void initAttributes(@NonNull Context context, @Nullable AttributeSet attrs) { + attributeController.init(context, attrs); + } +} diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/draw/controller/AttributeController.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/draw/controller/AttributeController.java new file mode 100644 index 000000000..0344d62d8 --- /dev/null +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/draw/controller/AttributeController.java @@ -0,0 +1,178 @@ +package com.smarteist.autoimageslider.IndicatorView.draw.controller; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Color; +import android.util.AttributeSet; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.smarteist.autoimageslider.IndicatorView.animation.type.BaseAnimation; +import com.smarteist.autoimageslider.IndicatorView.animation.type.ColorAnimation; +import com.smarteist.autoimageslider.IndicatorView.animation.type.FillAnimation; +import com.smarteist.autoimageslider.IndicatorView.animation.type.IndicatorAnimationType; +import com.smarteist.autoimageslider.IndicatorView.animation.type.ScaleAnimation; +import com.smarteist.autoimageslider.IndicatorView.draw.data.Indicator; +import com.smarteist.autoimageslider.IndicatorView.draw.data.Orientation; +import com.smarteist.autoimageslider.IndicatorView.draw.data.RtlMode; +import com.smarteist.autoimageslider.IndicatorView.utils.DensityUtils; +import com.smarteist.autoimageslider.R; + +public class AttributeController { + + private final Indicator indicator; + + public AttributeController(@NonNull Indicator indicator) { + this.indicator = indicator; + } + + public static RtlMode getRtlMode(int index) { + switch (index) { + case 0: + return RtlMode.On; + case 1: + return RtlMode.Off; + case 2: + return RtlMode.Auto; + } + + return RtlMode.Auto; + } + + public void init(@NonNull Context context, @Nullable AttributeSet attrs) { + TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.PageIndicatorView, 0, 0); + initCountAttribute(typedArray); + initColorAttribute(typedArray); + initAnimationAttribute(typedArray); + initSizeAttribute(typedArray); + typedArray.recycle(); + } + + private void initCountAttribute(@NonNull TypedArray typedArray) { + int viewPagerId = typedArray.getResourceId(R.styleable.PageIndicatorView_piv_viewPager, View.NO_ID); + boolean autoVisibility = typedArray.getBoolean(R.styleable.PageIndicatorView_piv_autoVisibility, true); + boolean dynamicCount = typedArray.getBoolean(R.styleable.PageIndicatorView_piv_dynamicCount, false); + int count = typedArray.getInt(R.styleable.PageIndicatorView_piv_count, Indicator.COUNT_NONE); + + if (count == Indicator.COUNT_NONE) { + count = Indicator.DEFAULT_COUNT; + } + + int position = typedArray.getInt(R.styleable.PageIndicatorView_piv_select, 0); + if (position < 0) { + position = 0; + } else if (count > 0 && position > count - 1) { + position = count - 1; + } + + indicator.setViewPagerId(viewPagerId); + indicator.setAutoVisibility(autoVisibility); + indicator.setDynamicCount(dynamicCount); + indicator.setCount(count); + + indicator.setSelectedPosition(position); + indicator.setSelectingPosition(position); + indicator.setLastSelectedPosition(position); + } + + private void initColorAttribute(@NonNull TypedArray typedArray) { + int unselectedColor = typedArray.getColor(R.styleable.PageIndicatorView_piv_unselectedColor, Color.parseColor(ColorAnimation.DEFAULT_UNSELECTED_COLOR)); + int selectedColor = typedArray.getColor(R.styleable.PageIndicatorView_piv_selectedColor, Color.parseColor(ColorAnimation.DEFAULT_SELECTED_COLOR)); + + indicator.setUnselectedColor(unselectedColor); + indicator.setSelectedColor(selectedColor); + } + + private void initAnimationAttribute(@NonNull TypedArray typedArray) { + boolean interactiveAnimation = typedArray.getBoolean(R.styleable.PageIndicatorView_piv_interactiveAnimation, false); + int animationDuration = typedArray.getInt(R.styleable.PageIndicatorView_piv_animationDuration, BaseAnimation.DEFAULT_ANIMATION_TIME); + if (animationDuration < 0) { + animationDuration = 0; + } + + int animIndex = typedArray.getInt(R.styleable.PageIndicatorView_piv_animationType, IndicatorAnimationType.NONE.ordinal()); + IndicatorAnimationType animationType = getAnimationType(animIndex); + + int rtlIndex = typedArray.getInt(R.styleable.PageIndicatorView_piv_rtl_mode, RtlMode.Off.ordinal()); + RtlMode rtlMode = getRtlMode(rtlIndex); + + indicator.setAnimationDuration(animationDuration); + indicator.setInteractiveAnimation(interactiveAnimation); + indicator.setAnimationType(animationType); + indicator.setRtlMode(rtlMode); + } + + private void initSizeAttribute(@NonNull TypedArray typedArray) { + int orientationIndex = typedArray.getInt(R.styleable.PageIndicatorView_piv_orientation, Orientation.HORIZONTAL.ordinal()); + Orientation orientation; + + if (orientationIndex == 0) { + orientation = Orientation.HORIZONTAL; + } else { + orientation = Orientation.VERTICAL; + } + + int radius = (int) typedArray.getDimension(R.styleable.PageIndicatorView_piv_radius, DensityUtils.dpToPx(Indicator.DEFAULT_RADIUS_DP)); + if (radius < 0) { + radius = 0; + } + + int padding = (int) typedArray.getDimension(R.styleable.PageIndicatorView_piv_padding, DensityUtils.dpToPx(Indicator.DEFAULT_PADDING_DP)); + if (padding < 0) { + padding = 0; + } + + float scaleFactor = typedArray.getFloat(R.styleable.PageIndicatorView_piv_scaleFactor, ScaleAnimation.DEFAULT_SCALE_FACTOR); + if (scaleFactor < ScaleAnimation.MIN_SCALE_FACTOR) { + scaleFactor = ScaleAnimation.MIN_SCALE_FACTOR; + + } else if (scaleFactor > ScaleAnimation.MAX_SCALE_FACTOR) { + scaleFactor = ScaleAnimation.MAX_SCALE_FACTOR; + } + + int stroke = (int) typedArray.getDimension(R.styleable.PageIndicatorView_piv_strokeWidth, DensityUtils.dpToPx(FillAnimation.DEFAULT_STROKE_DP)); + if (stroke > radius) { + stroke = radius; + } + + if (indicator.getAnimationType() != IndicatorAnimationType.FILL) { + stroke = 0; + } + + indicator.setRadius(radius); + indicator.setOrientation(orientation); + indicator.setPadding(padding); + indicator.setScaleFactor(scaleFactor); + indicator.setStroke(stroke); + } + + private IndicatorAnimationType getAnimationType(int index) { + switch (index) { + case 0: + return IndicatorAnimationType.NONE; + case 1: + return IndicatorAnimationType.COLOR; + case 2: + return IndicatorAnimationType.SCALE; + case 3: + return IndicatorAnimationType.WORM; + case 4: + return IndicatorAnimationType.SLIDE; + case 5: + return IndicatorAnimationType.FILL; + case 6: + return IndicatorAnimationType.THIN_WORM; + case 7: + return IndicatorAnimationType.DROP; + case 8: + return IndicatorAnimationType.SWAP; + case 9: + return IndicatorAnimationType.SCALE_DOWN; + } + + return IndicatorAnimationType.NONE; + } + +} diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/draw/controller/DrawController.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/draw/controller/DrawController.java new file mode 100644 index 000000000..174943183 --- /dev/null +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/draw/controller/DrawController.java @@ -0,0 +1,139 @@ +package com.smarteist.autoimageslider.IndicatorView.draw.controller; + +import android.graphics.Canvas; +import android.view.MotionEvent; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.smarteist.autoimageslider.IndicatorView.animation.data.Value; +import com.smarteist.autoimageslider.IndicatorView.animation.type.IndicatorAnimationType; +import com.smarteist.autoimageslider.IndicatorView.draw.data.Indicator; +import com.smarteist.autoimageslider.IndicatorView.draw.drawer.Drawer; +import com.smarteist.autoimageslider.IndicatorView.utils.CoordinatesUtils; + +public class DrawController { + + private final Drawer drawer; + private final Indicator indicator; + private Value value; + private ClickListener listener; + + public DrawController(@NonNull Indicator indicator) { + this.indicator = indicator; + this.drawer = new Drawer(indicator); + } + + public void updateValue(@Nullable Value value) { + this.value = value; + } + + public void setClickListener(@Nullable ClickListener listener) { + this.listener = listener; + } + + public void touch(@Nullable MotionEvent event) { + if (event == null) { + return; + } + + switch (event.getAction()) { + case MotionEvent.ACTION_UP: + onIndicatorTouched(event.getX(), event.getY()); + break; + default: + } + } + + private void onIndicatorTouched(float x, float y) { + if (listener != null) { + int position = CoordinatesUtils.getPosition(indicator, x, y); + if (position >= 0) { + listener.onIndicatorClicked(position); + } + } + } + + public void draw(@NonNull Canvas canvas) { + int count = indicator.getCount(); + + for (int position = 0; position < count; position++) { + int coordinateX = CoordinatesUtils.getXCoordinate(indicator, position); + int coordinateY = CoordinatesUtils.getYCoordinate(indicator, position); + drawIndicator(canvas, position, coordinateX, coordinateY); + } + } + + private void drawIndicator( + @NonNull Canvas canvas, + int position, + int coordinateX, + int coordinateY) { + + boolean interactiveAnimation = indicator.isInteractiveAnimation(); + int selectedPosition = indicator.getSelectedPosition(); + int selectingPosition = indicator.getSelectingPosition(); + int lastSelectedPosition = indicator.getLastSelectedPosition(); + + boolean selectedItem = !interactiveAnimation && (position == selectedPosition || position == lastSelectedPosition); + boolean selectingItem = interactiveAnimation && (position == selectedPosition || position == selectingPosition); + boolean isSelectedItem = selectedItem | selectingItem; + drawer.setup(position, coordinateX, coordinateY); + + if (value != null && isSelectedItem) { + drawWithAnimation(canvas); + } else { + drawer.drawBasic(canvas, isSelectedItem); + } + } + + private void drawWithAnimation(@NonNull Canvas canvas) { + IndicatorAnimationType animationType = indicator.getAnimationType(); + switch (animationType) { + case NONE: + drawer.drawBasic(canvas, true); + break; + + case COLOR: + drawer.drawColor(canvas, value); + break; + + case SCALE: + drawer.drawScale(canvas, value); + break; + + case WORM: + drawer.drawWorm(canvas, value); + break; + + case SLIDE: + drawer.drawSlide(canvas, value); + break; + + case FILL: + drawer.drawFill(canvas, value); + break; + + case THIN_WORM: + drawer.drawThinWorm(canvas, value); + break; + + case DROP: + drawer.drawDrop(canvas, value); + break; + + case SWAP: + drawer.drawSwap(canvas, value); + break; + + case SCALE_DOWN: + drawer.drawScaleDown(canvas, value); + break; + } + } + + public interface ClickListener { + + void onIndicatorClicked(int position); + } +} diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/draw/controller/MeasureController.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/draw/controller/MeasureController.java new file mode 100644 index 000000000..e454b81e7 --- /dev/null +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/draw/controller/MeasureController.java @@ -0,0 +1,106 @@ +package com.smarteist.autoimageslider.IndicatorView.draw.controller; + +import android.util.Pair; +import android.view.View; + +import androidx.annotation.NonNull; + +import com.smarteist.autoimageslider.IndicatorView.animation.type.IndicatorAnimationType; +import com.smarteist.autoimageslider.IndicatorView.draw.data.Indicator; +import com.smarteist.autoimageslider.IndicatorView.draw.data.Orientation; + +public class MeasureController { + + public Pair measureViewSize(@NonNull Indicator indicator, int widthMeasureSpec, int heightMeasureSpec) { + int widthMode = View.MeasureSpec.getMode(widthMeasureSpec); + int widthSize = View.MeasureSpec.getSize(widthMeasureSpec); + + int heightMode = View.MeasureSpec.getMode(heightMeasureSpec); + int heightSize = View.MeasureSpec.getSize(heightMeasureSpec); + + int count = indicator.getCount(); + int radius = indicator.getRadius(); + int stroke = indicator.getStroke(); + + int padding = indicator.getPadding(); + int paddingLeft = indicator.getPaddingLeft(); + int paddingTop = indicator.getPaddingTop(); + int paddingRight = indicator.getPaddingRight(); + int paddingBottom = indicator.getPaddingBottom(); + + int circleDiameterPx = radius * 2; + int desiredWidth = 0; + int desiredHeight = 0; + + int width; + int height; + + Orientation orientation = indicator.getOrientation(); + if (count != 0) { + int diameterSum = circleDiameterPx * count; + int strokeSum = (stroke * 2) * count; + + int paddingSum = padding * (count - 1); + int w = diameterSum + strokeSum + paddingSum; + int h = circleDiameterPx + stroke; + + if (orientation == Orientation.HORIZONTAL) { + desiredWidth = w; + desiredHeight = h; + + } else { + desiredWidth = h; + desiredHeight = w; + } + } + + if (indicator.getAnimationType() == IndicatorAnimationType.DROP) { + if (orientation == Orientation.HORIZONTAL) { + desiredHeight *= 2; + } else { + desiredWidth *= 2; + } + } + + int horizontalPadding = paddingLeft + paddingRight; + int verticalPadding = paddingTop + paddingBottom; + + if (orientation == Orientation.HORIZONTAL) { + desiredWidth += horizontalPadding; + desiredHeight += verticalPadding; + + } else { + desiredWidth += horizontalPadding; + desiredHeight += verticalPadding; + } + + if (widthMode == View.MeasureSpec.EXACTLY) { + width = widthSize; + } else if (widthMode == View.MeasureSpec.AT_MOST) { + width = Math.min(desiredWidth, widthSize); + } else { + width = desiredWidth; + } + + if (heightMode == View.MeasureSpec.EXACTLY) { + height = heightSize; + } else if (heightMode == View.MeasureSpec.AT_MOST) { + height = Math.min(desiredHeight, heightSize); + } else { + height = desiredHeight; + } + + if (width < 0) { + width = 0; + } + + if (height < 0) { + height = 0; + } + + indicator.setWidth(width); + indicator.setHeight(height); + + return new Pair<>(width, height); + } +} diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/draw/data/Indicator.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/draw/data/Indicator.java new file mode 100644 index 000000000..3d60114d3 --- /dev/null +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/draw/data/Indicator.java @@ -0,0 +1,254 @@ +package com.smarteist.autoimageslider.IndicatorView.draw.data; + +import android.view.View; + +import androidx.annotation.NonNull; + +import com.smarteist.autoimageslider.IndicatorView.animation.type.IndicatorAnimationType; + +public class Indicator { + + public static final int DEFAULT_COUNT = 3; + public static final int MIN_COUNT = 1; + public static final int COUNT_NONE = -1; + + public static final int DEFAULT_RADIUS_DP = 6; + public static final int DEFAULT_PADDING_DP = 8; + + private int height; + private int width; + private int radius; + + private int padding; + private int paddingLeft; + private int paddingTop; + private int paddingRight; + private int paddingBottom; + + private int stroke; //For "Fill" animation only + private float scaleFactor; //For "Scale" animation only + + private int unselectedColor; + private int selectedColor; + + private boolean interactiveAnimation; + private boolean autoVisibility; + private boolean dynamicCount; + + private long animationDuration; + private int count = DEFAULT_COUNT; + + private int selectedPosition; + private int selectingPosition; + private int lastSelectedPosition; + + private int viewPagerId = View.NO_ID; + + private Orientation orientation; + private IndicatorAnimationType animationType; + private RtlMode rtlMode; + + public int getHeight() { + return height; + } + + public void setHeight(int height) { + this.height = height; + } + + public int getWidth() { + return width; + } + + public void setWidth(int width) { + this.width = width; + } + + public int getRadius() { + return radius; + } + + public void setRadius(int radius) { + this.radius = radius; + } + + public int getPadding() { + return padding; + } + + public void setPadding(int padding) { + this.padding = padding; + } + + public int getPaddingLeft() { + return paddingLeft; + } + + public void setPaddingLeft(int paddingLeft) { + this.paddingLeft = paddingLeft; + } + + public int getPaddingTop() { + return paddingTop; + } + + public void setPaddingTop(int paddingTop) { + this.paddingTop = paddingTop; + } + + public int getPaddingRight() { + return paddingRight; + } + + public void setPaddingRight(int paddingRight) { + this.paddingRight = paddingRight; + } + + public int getPaddingBottom() { + return paddingBottom; + } + + public void setPaddingBottom(int paddingBottom) { + this.paddingBottom = paddingBottom; + } + + public int getStroke() { + return stroke; + } + + public void setStroke(int stroke) { + this.stroke = stroke; + } + + public float getScaleFactor() { + return scaleFactor; + } + + public void setScaleFactor(float scaleFactor) { + this.scaleFactor = scaleFactor; + } + + public int getUnselectedColor() { + return unselectedColor; + } + + public void setUnselectedColor(int unselectedColor) { + this.unselectedColor = unselectedColor; + } + + public int getSelectedColor() { + return selectedColor; + } + + public void setSelectedColor(int selectedColor) { + this.selectedColor = selectedColor; + } + + public boolean isInteractiveAnimation() { + return interactiveAnimation; + } + + public void setInteractiveAnimation(boolean interactiveAnimation) { + this.interactiveAnimation = interactiveAnimation; + } + + public boolean isAutoVisibility() { + return autoVisibility; + } + + public void setAutoVisibility(boolean autoVisibility) { + this.autoVisibility = autoVisibility; + } + + public boolean isDynamicCount() { + return dynamicCount; + } + + public void setDynamicCount(boolean dynamicCount) { + this.dynamicCount = dynamicCount; + } + + public long getAnimationDuration() { + return animationDuration; + } + + public void setAnimationDuration(long animationDuration) { + this.animationDuration = animationDuration; + } + + public int getSelectedPosition() { + return selectedPosition; + } + + public void setSelectedPosition(int selectedPosition) { + this.selectedPosition = selectedPosition; + } + + public int getSelectingPosition() { + return selectingPosition; + } + + public void setSelectingPosition(int selectingPosition) { + this.selectingPosition = selectingPosition; + } + + public int getLastSelectedPosition() { + return lastSelectedPosition; + } + + public void setLastSelectedPosition(int lastSelectedPosition) { + this.lastSelectedPosition = lastSelectedPosition; + } + + public int getCount() { + return count; + } + + public void setCount(int count) { + this.count = count; + } + + @NonNull + public Orientation getOrientation() { + if (orientation == null) { + orientation = Orientation.HORIZONTAL; + } + return orientation; + } + + public void setOrientation(Orientation orientation) { + this.orientation = orientation; + } + + @NonNull + public IndicatorAnimationType getAnimationType() { + if (animationType == null) { + animationType = IndicatorAnimationType.NONE; + } + return animationType; + } + + public void setAnimationType(IndicatorAnimationType animationType) { + this.animationType = animationType; + } + + @NonNull + public RtlMode getRtlMode() { + if (rtlMode == null) { + rtlMode = RtlMode.Off; + } + return rtlMode; + } + + public void setRtlMode(RtlMode rtlMode) { + this.rtlMode = rtlMode; + } + + public int getViewPagerId() { + return viewPagerId; + } + + public void setViewPagerId(int viewPagerId) { + this.viewPagerId = viewPagerId; + } +} diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/draw/data/Orientation.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/draw/data/Orientation.java new file mode 100644 index 000000000..3cb7a3f54 --- /dev/null +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/draw/data/Orientation.java @@ -0,0 +1,3 @@ +package com.smarteist.autoimageslider.IndicatorView.draw.data; + +public enum Orientation {HORIZONTAL, VERTICAL} diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/draw/data/PositionSavedState.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/draw/data/PositionSavedState.java new file mode 100644 index 000000000..9245b11f6 --- /dev/null +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/draw/data/PositionSavedState.java @@ -0,0 +1,64 @@ +package com.smarteist.autoimageslider.IndicatorView.draw.data; + +import android.os.Parcel; +import android.os.Parcelable; +import android.view.View; + +public class PositionSavedState extends View.BaseSavedState { + + public static final Creator CREATOR = new Creator() { + public PositionSavedState createFromParcel(Parcel in) { + return new PositionSavedState(in); + } + + public PositionSavedState[] newArray(int size) { + return new PositionSavedState[size]; + } + }; + private int selectedPosition; + private int selectingPosition; + private int lastSelectedPosition; + + public PositionSavedState(Parcelable superState) { + super(superState); + } + + private PositionSavedState(Parcel in) { + super(in); + this.selectedPosition = in.readInt(); + this.selectingPosition = in.readInt(); + this.lastSelectedPosition = in.readInt(); + } + + public int getSelectedPosition() { + return selectedPosition; + } + + public void setSelectedPosition(int selectedPosition) { + this.selectedPosition = selectedPosition; + } + + public int getSelectingPosition() { + return selectingPosition; + } + + public void setSelectingPosition(int selectingPosition) { + this.selectingPosition = selectingPosition; + } + + public int getLastSelectedPosition() { + return lastSelectedPosition; + } + + public void setLastSelectedPosition(int lastSelectedPosition) { + this.lastSelectedPosition = lastSelectedPosition; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + super.writeToParcel(out, flags); + out.writeInt(this.selectedPosition); + out.writeInt(this.selectingPosition); + out.writeInt(this.lastSelectedPosition); + } +} diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/draw/data/RtlMode.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/draw/data/RtlMode.java new file mode 100644 index 000000000..96fd96540 --- /dev/null +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/draw/data/RtlMode.java @@ -0,0 +1,3 @@ +package com.smarteist.autoimageslider.IndicatorView.draw.data; + +public enum RtlMode {On, Off, Auto} \ No newline at end of file diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/draw/drawer/Drawer.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/draw/drawer/Drawer.java new file mode 100644 index 000000000..83353f35b --- /dev/null +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/draw/drawer/Drawer.java @@ -0,0 +1,120 @@ +package com.smarteist.autoimageslider.IndicatorView.draw.drawer; + +import android.graphics.Canvas; +import android.graphics.Paint; + +import androidx.annotation.NonNull; + +import com.smarteist.autoimageslider.IndicatorView.animation.data.Value; +import com.smarteist.autoimageslider.IndicatorView.draw.data.Indicator; +import com.smarteist.autoimageslider.IndicatorView.draw.drawer.type.BasicDrawer; +import com.smarteist.autoimageslider.IndicatorView.draw.drawer.type.ColorDrawer; +import com.smarteist.autoimageslider.IndicatorView.draw.drawer.type.DropDrawer; +import com.smarteist.autoimageslider.IndicatorView.draw.drawer.type.FillDrawer; +import com.smarteist.autoimageslider.IndicatorView.draw.drawer.type.ScaleDownDrawer; +import com.smarteist.autoimageslider.IndicatorView.draw.drawer.type.ScaleDrawer; +import com.smarteist.autoimageslider.IndicatorView.draw.drawer.type.SlideDrawer; +import com.smarteist.autoimageslider.IndicatorView.draw.drawer.type.SwapDrawer; +import com.smarteist.autoimageslider.IndicatorView.draw.drawer.type.ThinWormDrawer; +import com.smarteist.autoimageslider.IndicatorView.draw.drawer.type.WormDrawer; + +public class Drawer { + + private final BasicDrawer basicDrawer; + private final ColorDrawer colorDrawer; + private final ScaleDrawer scaleDrawer; + private final WormDrawer wormDrawer; + private final SlideDrawer slideDrawer; + private final FillDrawer fillDrawer; + private final ThinWormDrawer thinWormDrawer; + private final DropDrawer dropDrawer; + private final SwapDrawer swapDrawer; + private final ScaleDownDrawer scaleDownDrawer; + + private int position; + private int coordinateX; + private int coordinateY; + + public Drawer(@NonNull Indicator indicator) { + Paint paint = new Paint(); + paint.setStyle(Paint.Style.FILL); + paint.setAntiAlias(true); + + basicDrawer = new BasicDrawer(paint, indicator); + colorDrawer = new ColorDrawer(paint, indicator); + scaleDrawer = new ScaleDrawer(paint, indicator); + wormDrawer = new WormDrawer(paint, indicator); + slideDrawer = new SlideDrawer(paint, indicator); + fillDrawer = new FillDrawer(paint, indicator); + thinWormDrawer = new ThinWormDrawer(paint, indicator); + dropDrawer = new DropDrawer(paint, indicator); + swapDrawer = new SwapDrawer(paint, indicator); + scaleDownDrawer = new ScaleDownDrawer(paint, indicator); + } + + public void setup(int position, int coordinateX, int coordinateY) { + this.position = position; + this.coordinateX = coordinateX; + this.coordinateY = coordinateY; + } + + public void drawBasic(@NonNull Canvas canvas, boolean isSelectedItem) { + if (colorDrawer != null) { + basicDrawer.draw(canvas, position, isSelectedItem, coordinateX, coordinateY); + } + } + + public void drawColor(@NonNull Canvas canvas, @NonNull Value value) { + if (colorDrawer != null) { + colorDrawer.draw(canvas, value, position, coordinateX, coordinateY); + } + } + + public void drawScale(@NonNull Canvas canvas, @NonNull Value value) { + if (scaleDrawer != null) { + scaleDrawer.draw(canvas, value, position, coordinateX, coordinateY); + } + } + + public void drawWorm(@NonNull Canvas canvas, @NonNull Value value) { + if (wormDrawer != null) { + wormDrawer.draw(canvas, value, coordinateX, coordinateY); + } + } + + public void drawSlide(@NonNull Canvas canvas, @NonNull Value value) { + if (slideDrawer != null) { + slideDrawer.draw(canvas, value, coordinateX, coordinateY); + } + } + + public void drawFill(@NonNull Canvas canvas, @NonNull Value value) { + if (fillDrawer != null) { + fillDrawer.draw(canvas, value, position, coordinateX, coordinateY); + } + } + + public void drawThinWorm(@NonNull Canvas canvas, @NonNull Value value) { + if (thinWormDrawer != null) { + thinWormDrawer.draw(canvas, value, coordinateX, coordinateY); + } + } + + public void drawDrop(@NonNull Canvas canvas, @NonNull Value value) { + if (dropDrawer != null) { + dropDrawer.draw(canvas, value, coordinateX, coordinateY); + } + } + + public void drawSwap(@NonNull Canvas canvas, @NonNull Value value) { + if (swapDrawer != null) { + swapDrawer.draw(canvas, value, position, coordinateX, coordinateY); + } + } + + public void drawScaleDown(@NonNull Canvas canvas, @NonNull Value value) { + if (scaleDownDrawer != null) { + scaleDownDrawer.draw(canvas, value, position, coordinateX, coordinateY); + } + } +} diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/draw/drawer/type/BaseDrawer.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/draw/drawer/type/BaseDrawer.java new file mode 100644 index 000000000..ae0bea5a7 --- /dev/null +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/draw/drawer/type/BaseDrawer.java @@ -0,0 +1,18 @@ +package com.smarteist.autoimageslider.IndicatorView.draw.drawer.type; + +import android.graphics.Paint; + +import androidx.annotation.NonNull; + +import com.smarteist.autoimageslider.IndicatorView.draw.data.Indicator; + +class BaseDrawer { + + Paint paint; + Indicator indicator; + + BaseDrawer(@NonNull Paint paint, @NonNull Indicator indicator) { + this.paint = paint; + this.indicator = indicator; + } +} diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/draw/drawer/type/BasicDrawer.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/draw/drawer/type/BasicDrawer.java new file mode 100644 index 000000000..1c676bd25 --- /dev/null +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/draw/drawer/type/BasicDrawer.java @@ -0,0 +1,63 @@ +package com.smarteist.autoimageslider.IndicatorView.draw.drawer.type; + +import android.graphics.Canvas; +import android.graphics.Paint; + +import androidx.annotation.NonNull; + +import com.smarteist.autoimageslider.IndicatorView.animation.type.IndicatorAnimationType; +import com.smarteist.autoimageslider.IndicatorView.draw.data.Indicator; + +public class BasicDrawer extends BaseDrawer { + + private final Paint strokePaint; + + public BasicDrawer(@NonNull Paint paint, @NonNull Indicator indicator) { + super(paint, indicator); + + strokePaint = new Paint(); + strokePaint.setStyle(Paint.Style.STROKE); + strokePaint.setAntiAlias(true); + strokePaint.setStrokeWidth(indicator.getStroke()); + } + + public void draw( + @NonNull Canvas canvas, + int position, + boolean isSelectedItem, + int coordinateX, + int coordinateY) { + + float radius = indicator.getRadius(); + int strokePx = indicator.getStroke(); + float scaleFactor = indicator.getScaleFactor(); + + int selectedColor = indicator.getSelectedColor(); + int unselectedColor = indicator.getUnselectedColor(); + int selectedPosition = indicator.getSelectedPosition(); + IndicatorAnimationType animationType = indicator.getAnimationType(); + + if (animationType == IndicatorAnimationType.SCALE && !isSelectedItem) { + radius *= scaleFactor; + + } else if (animationType == IndicatorAnimationType.SCALE_DOWN && isSelectedItem) { + radius *= scaleFactor; + } + + int color = unselectedColor; + if (position == selectedPosition) { + color = selectedColor; + } + + Paint paint; + if (animationType == IndicatorAnimationType.FILL && position != selectedPosition) { + paint = strokePaint; + paint.setStrokeWidth(strokePx); + } else { + paint = this.paint; + } + + paint.setColor(color); + canvas.drawCircle(coordinateX, coordinateY, radius, paint); + } +} diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/draw/drawer/type/ColorDrawer.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/draw/drawer/type/ColorDrawer.java new file mode 100644 index 000000000..083c69f94 --- /dev/null +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/draw/drawer/type/ColorDrawer.java @@ -0,0 +1,56 @@ +package com.smarteist.autoimageslider.IndicatorView.draw.drawer.type; + +import android.graphics.Canvas; +import android.graphics.Paint; + +import androidx.annotation.NonNull; + +import com.smarteist.autoimageslider.IndicatorView.animation.data.Value; +import com.smarteist.autoimageslider.IndicatorView.animation.data.type.ColorAnimationValue; +import com.smarteist.autoimageslider.IndicatorView.draw.data.Indicator; + +public class ColorDrawer extends BaseDrawer { + + public ColorDrawer(@NonNull Paint paint, @NonNull Indicator indicator) { + super(paint, indicator); + } + + public void draw(@NonNull Canvas canvas, + @NonNull Value value, + int position, + int coordinateX, + int coordinateY) { + + if (!(value instanceof ColorAnimationValue)) { + return; + } + + ColorAnimationValue v = (ColorAnimationValue) value; + float radius = indicator.getRadius(); + int color = indicator.getSelectedColor(); + + int selectedPosition = indicator.getSelectedPosition(); + int selectingPosition = indicator.getSelectingPosition(); + int lastSelectedPosition = indicator.getLastSelectedPosition(); + + if (indicator.isInteractiveAnimation()) { + if (position == selectingPosition) { + color = v.getColor(); + + } else if (position == selectedPosition) { + color = v.getColorReverse(); + } + + } else { + if (position == selectedPosition) { + color = v.getColor(); + + } else if (position == lastSelectedPosition) { + color = v.getColorReverse(); + } + } + + paint.setColor(color); + canvas.drawCircle(coordinateX, coordinateY, radius, paint); + } +} diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/draw/drawer/type/DropDrawer.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/draw/drawer/type/DropDrawer.java new file mode 100644 index 000000000..b5865a94b --- /dev/null +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/draw/drawer/type/DropDrawer.java @@ -0,0 +1,44 @@ +package com.smarteist.autoimageslider.IndicatorView.draw.drawer.type; + +import android.graphics.Canvas; +import android.graphics.Paint; + +import androidx.annotation.NonNull; + +import com.smarteist.autoimageslider.IndicatorView.animation.data.Value; +import com.smarteist.autoimageslider.IndicatorView.animation.data.type.DropAnimationValue; +import com.smarteist.autoimageslider.IndicatorView.draw.data.Indicator; +import com.smarteist.autoimageslider.IndicatorView.draw.data.Orientation; + +public class DropDrawer extends BaseDrawer { + + public DropDrawer(@NonNull Paint paint, @NonNull Indicator indicator) { + super(paint, indicator); + } + + public void draw( + @NonNull Canvas canvas, + @NonNull Value value, + int coordinateX, + int coordinateY) { + + if (!(value instanceof DropAnimationValue)) { + return; + } + + DropAnimationValue v = (DropAnimationValue) value; + int unselectedColor = indicator.getUnselectedColor(); + int selectedColor = indicator.getSelectedColor(); + float radius = indicator.getRadius(); + + paint.setColor(unselectedColor); + canvas.drawCircle(coordinateX, coordinateY, radius, paint); + + paint.setColor(selectedColor); + if (indicator.getOrientation() == Orientation.HORIZONTAL) { + canvas.drawCircle(v.getWidth(), v.getHeight(), v.getRadius(), paint); + } else { + canvas.drawCircle(v.getHeight(), v.getWidth(), v.getRadius(), paint); + } + } +} diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/draw/drawer/type/FillDrawer.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/draw/drawer/type/FillDrawer.java new file mode 100644 index 000000000..25a442603 --- /dev/null +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/draw/drawer/type/FillDrawer.java @@ -0,0 +1,76 @@ +package com.smarteist.autoimageslider.IndicatorView.draw.drawer.type; + +import android.graphics.Canvas; +import android.graphics.Paint; + +import androidx.annotation.NonNull; + +import com.smarteist.autoimageslider.IndicatorView.animation.data.Value; +import com.smarteist.autoimageslider.IndicatorView.animation.data.type.FillAnimationValue; +import com.smarteist.autoimageslider.IndicatorView.draw.data.Indicator; + +public class FillDrawer extends BaseDrawer { + + private final Paint strokePaint; + + public FillDrawer(@NonNull Paint paint, @NonNull Indicator indicator) { + super(paint, indicator); + + strokePaint = new Paint(); + strokePaint.setStyle(Paint.Style.STROKE); + strokePaint.setAntiAlias(true); + } + + public void draw( + @NonNull Canvas canvas, + @NonNull Value value, + int position, + int coordinateX, + int coordinateY) { + + if (!(value instanceof FillAnimationValue)) { + return; + } + + FillAnimationValue v = (FillAnimationValue) value; + int color = indicator.getUnselectedColor(); + float radius = indicator.getRadius(); + int stroke = indicator.getStroke(); + + int selectedPosition = indicator.getSelectedPosition(); + int selectingPosition = indicator.getSelectingPosition(); + int lastSelectedPosition = indicator.getLastSelectedPosition(); + + if (indicator.isInteractiveAnimation()) { + if (position == selectingPosition) { + color = v.getColor(); + radius = v.getRadius(); + stroke = v.getStroke(); + + } else if (position == selectedPosition) { + color = v.getColorReverse(); + radius = v.getRadiusReverse(); + stroke = v.getStrokeReverse(); + } + + } else { + if (position == selectedPosition) { + color = v.getColor(); + radius = v.getRadius(); + stroke = v.getStroke(); + + } else if (position == lastSelectedPosition) { + color = v.getColorReverse(); + radius = v.getRadiusReverse(); + stroke = v.getStrokeReverse(); + } + } + + strokePaint.setColor(color); + strokePaint.setStrokeWidth(indicator.getStroke()); + canvas.drawCircle(coordinateX, coordinateY, indicator.getRadius(), strokePaint); + + strokePaint.setStrokeWidth(stroke); + canvas.drawCircle(coordinateX, coordinateY, radius, strokePaint); + } +} diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/draw/drawer/type/ScaleDownDrawer.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/draw/drawer/type/ScaleDownDrawer.java new file mode 100644 index 000000000..163f12b3b --- /dev/null +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/draw/drawer/type/ScaleDownDrawer.java @@ -0,0 +1,61 @@ +package com.smarteist.autoimageslider.IndicatorView.draw.drawer.type; + +import android.graphics.Canvas; +import android.graphics.Paint; + +import androidx.annotation.NonNull; + +import com.smarteist.autoimageslider.IndicatorView.animation.data.Value; +import com.smarteist.autoimageslider.IndicatorView.animation.data.type.ScaleAnimationValue; +import com.smarteist.autoimageslider.IndicatorView.draw.data.Indicator; + +public class ScaleDownDrawer extends BaseDrawer { + + public ScaleDownDrawer(@NonNull Paint paint, @NonNull Indicator indicator) { + super(paint, indicator); + } + + public void draw( + @NonNull Canvas canvas, + @NonNull Value value, + int position, + int coordinateX, + int coordinateY) { + + if (!(value instanceof ScaleAnimationValue)) { + return; + } + + ScaleAnimationValue v = (ScaleAnimationValue) value; + float radius = indicator.getRadius(); + int color = indicator.getSelectedColor(); + + int selectedPosition = indicator.getSelectedPosition(); + int selectingPosition = indicator.getSelectingPosition(); + int lastSelectedPosition = indicator.getLastSelectedPosition(); + + if (indicator.isInteractiveAnimation()) { + if (position == selectingPosition) { + radius = v.getRadius(); + color = v.getColor(); + + } else if (position == selectedPosition) { + radius = v.getRadiusReverse(); + color = v.getColorReverse(); + } + + } else { + if (position == selectedPosition) { + radius = v.getRadius(); + color = v.getColor(); + + } else if (position == lastSelectedPosition) { + radius = v.getRadiusReverse(); + color = v.getColorReverse(); + } + } + + paint.setColor(color); + canvas.drawCircle(coordinateX, coordinateY, radius, paint); + } +} diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/draw/drawer/type/ScaleDrawer.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/draw/drawer/type/ScaleDrawer.java new file mode 100644 index 000000000..629fe7cea --- /dev/null +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/draw/drawer/type/ScaleDrawer.java @@ -0,0 +1,61 @@ +package com.smarteist.autoimageslider.IndicatorView.draw.drawer.type; + +import android.graphics.Canvas; +import android.graphics.Paint; + +import androidx.annotation.NonNull; + +import com.smarteist.autoimageslider.IndicatorView.animation.data.Value; +import com.smarteist.autoimageslider.IndicatorView.animation.data.type.ScaleAnimationValue; +import com.smarteist.autoimageslider.IndicatorView.draw.data.Indicator; + +public class ScaleDrawer extends BaseDrawer { + + public ScaleDrawer(@NonNull Paint paint, @NonNull Indicator indicator) { + super(paint, indicator); + } + + public void draw( + @NonNull Canvas canvas, + @NonNull Value value, + int position, + int coordinateX, + int coordinateY) { + + if (!(value instanceof ScaleAnimationValue)) { + return; + } + + ScaleAnimationValue v = (ScaleAnimationValue) value; + float radius = indicator.getRadius(); + int color = indicator.getSelectedColor(); + + int selectedPosition = indicator.getSelectedPosition(); + int selectingPosition = indicator.getSelectingPosition(); + int lastSelectedPosition = indicator.getLastSelectedPosition(); + + if (indicator.isInteractiveAnimation()) { + if (position == selectingPosition) { + radius = v.getRadius(); + color = v.getColor(); + + } else if (position == selectedPosition) { + radius = v.getRadiusReverse(); + color = v.getColorReverse(); + } + + } else { + if (position == selectedPosition) { + radius = v.getRadius(); + color = v.getColor(); + + } else if (position == lastSelectedPosition) { + radius = v.getRadiusReverse(); + color = v.getColorReverse(); + } + } + + paint.setColor(color); + canvas.drawCircle(coordinateX, coordinateY, radius, paint); + } +} diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/draw/drawer/type/SlideDrawer.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/draw/drawer/type/SlideDrawer.java new file mode 100644 index 000000000..5c176f118 --- /dev/null +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/draw/drawer/type/SlideDrawer.java @@ -0,0 +1,45 @@ +package com.smarteist.autoimageslider.IndicatorView.draw.drawer.type; + +import android.graphics.Canvas; +import android.graphics.Paint; + +import androidx.annotation.NonNull; + +import com.smarteist.autoimageslider.IndicatorView.animation.data.Value; +import com.smarteist.autoimageslider.IndicatorView.animation.data.type.SlideAnimationValue; +import com.smarteist.autoimageslider.IndicatorView.draw.data.Indicator; +import com.smarteist.autoimageslider.IndicatorView.draw.data.Orientation; + +public class SlideDrawer extends BaseDrawer { + + public SlideDrawer(@NonNull Paint paint, @NonNull Indicator indicator) { + super(paint, indicator); + } + + public void draw( + @NonNull Canvas canvas, + @NonNull Value value, + int coordinateX, + int coordinateY) { + + if (!(value instanceof SlideAnimationValue)) { + return; + } + + SlideAnimationValue v = (SlideAnimationValue) value; + int coordinate = v.getCoordinate(); + int unselectedColor = indicator.getUnselectedColor(); + int selectedColor = indicator.getSelectedColor(); + int radius = indicator.getRadius(); + + paint.setColor(unselectedColor); + canvas.drawCircle(coordinateX, coordinateY, radius, paint); + + paint.setColor(selectedColor); + if (indicator.getOrientation() == Orientation.HORIZONTAL) { + canvas.drawCircle(coordinate, coordinateY, radius, paint); + } else { + canvas.drawCircle(coordinateX, coordinate, radius, paint); + } + } +} diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/draw/drawer/type/SwapDrawer.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/draw/drawer/type/SwapDrawer.java new file mode 100644 index 000000000..821e16617 --- /dev/null +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/draw/drawer/type/SwapDrawer.java @@ -0,0 +1,70 @@ +package com.smarteist.autoimageslider.IndicatorView.draw.drawer.type; + +import android.graphics.Canvas; +import android.graphics.Paint; + +import androidx.annotation.NonNull; + +import com.smarteist.autoimageslider.IndicatorView.animation.data.Value; +import com.smarteist.autoimageslider.IndicatorView.animation.data.type.SwapAnimationValue; +import com.smarteist.autoimageslider.IndicatorView.draw.data.Indicator; +import com.smarteist.autoimageslider.IndicatorView.draw.data.Orientation; + +public class SwapDrawer extends BaseDrawer { + + public SwapDrawer(@NonNull Paint paint, @NonNull Indicator indicator) { + super(paint, indicator); + } + + public void draw( + @NonNull Canvas canvas, + @NonNull Value value, + int position, + int coordinateX, + int coordinateY) { + + if (!(value instanceof SwapAnimationValue)) { + return; + } + + SwapAnimationValue v = (SwapAnimationValue) value; + int selectedColor = indicator.getSelectedColor(); + int unselectedColor = indicator.getUnselectedColor(); + int radius = indicator.getRadius(); + + int selectedPosition = indicator.getSelectedPosition(); + int selectingPosition = indicator.getSelectingPosition(); + int lastSelectedPosition = indicator.getLastSelectedPosition(); + + int coordinate = v.getCoordinate(); + int color = unselectedColor; + + if (indicator.isInteractiveAnimation()) { + if (position == selectingPosition) { + coordinate = v.getCoordinate(); + color = selectedColor; + + } else if (position == selectedPosition) { + coordinate = v.getCoordinateReverse(); + color = unselectedColor; + } + + } else { + if (position == lastSelectedPosition) { + coordinate = v.getCoordinate(); + color = selectedColor; + + } else if (position == selectedPosition) { + coordinate = v.getCoordinateReverse(); + color = unselectedColor; + } + } + + paint.setColor(color); + if (indicator.getOrientation() == Orientation.HORIZONTAL) { + canvas.drawCircle(coordinate, coordinateY, radius, paint); + } else { + canvas.drawCircle(coordinateX, coordinate, radius, paint); + } + } +} diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/draw/drawer/type/ThinWormDrawer.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/draw/drawer/type/ThinWormDrawer.java new file mode 100644 index 000000000..2b42dedea --- /dev/null +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/draw/drawer/type/ThinWormDrawer.java @@ -0,0 +1,57 @@ +package com.smarteist.autoimageslider.IndicatorView.draw.drawer.type; + +import android.graphics.Canvas; +import android.graphics.Paint; + +import androidx.annotation.NonNull; + +import com.smarteist.autoimageslider.IndicatorView.animation.data.Value; +import com.smarteist.autoimageslider.IndicatorView.animation.data.type.ThinWormAnimationValue; +import com.smarteist.autoimageslider.IndicatorView.draw.data.Indicator; +import com.smarteist.autoimageslider.IndicatorView.draw.data.Orientation; + +public class ThinWormDrawer extends WormDrawer { + + public ThinWormDrawer(@NonNull Paint paint, @NonNull Indicator indicator) { + super(paint, indicator); + } + + public void draw( + @NonNull Canvas canvas, + @NonNull Value value, + int coordinateX, + int coordinateY) { + + if (!(value instanceof ThinWormAnimationValue)) { + return; + } + + ThinWormAnimationValue v = (ThinWormAnimationValue) value; + int rectStart = v.getRectStart(); + int rectEnd = v.getRectEnd(); + int height = v.getHeight() / 2; + + int radius = indicator.getRadius(); + int unselectedColor = indicator.getUnselectedColor(); + int selectedColor = indicator.getSelectedColor(); + + if (indicator.getOrientation() == Orientation.HORIZONTAL) { + rect.left = rectStart; + rect.right = rectEnd; + rect.top = coordinateY - height; + rect.bottom = coordinateY + height; + + } else { + rect.left = coordinateX - height; + rect.right = coordinateX + height; + rect.top = rectStart; + rect.bottom = rectEnd; + } + + paint.setColor(unselectedColor); + canvas.drawCircle(coordinateX, coordinateY, radius, paint); + + paint.setColor(selectedColor); + canvas.drawRoundRect(rect, radius, radius, paint); + } +} diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/draw/drawer/type/WormDrawer.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/draw/drawer/type/WormDrawer.java new file mode 100644 index 000000000..31f174acf --- /dev/null +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/draw/drawer/type/WormDrawer.java @@ -0,0 +1,60 @@ +package com.smarteist.autoimageslider.IndicatorView.draw.drawer.type; + +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.RectF; + +import androidx.annotation.NonNull; + +import com.smarteist.autoimageslider.IndicatorView.animation.data.Value; +import com.smarteist.autoimageslider.IndicatorView.animation.data.type.WormAnimationValue; +import com.smarteist.autoimageslider.IndicatorView.draw.data.Indicator; +import com.smarteist.autoimageslider.IndicatorView.draw.data.Orientation; + +public class WormDrawer extends BaseDrawer { + + public RectF rect; + + public WormDrawer(@NonNull Paint paint, @NonNull Indicator indicator) { + super(paint, indicator); + rect = new RectF(); + } + + public void draw( + @NonNull Canvas canvas, + @NonNull Value value, + int coordinateX, + int coordinateY) { + + if (!(value instanceof WormAnimationValue)) { + return; + } + + WormAnimationValue v = (WormAnimationValue) value; + int rectStart = v.getRectStart(); + int rectEnd = v.getRectEnd(); + + int radius = indicator.getRadius(); + int unselectedColor = indicator.getUnselectedColor(); + int selectedColor = indicator.getSelectedColor(); + + if (indicator.getOrientation() == Orientation.HORIZONTAL) { + rect.left = rectStart; + rect.right = rectEnd; + rect.top = coordinateY - radius; + rect.bottom = coordinateY + radius; + + } else { + rect.left = coordinateX - radius; + rect.right = coordinateX + radius; + rect.top = rectStart; + rect.bottom = rectEnd; + } + + paint.setColor(unselectedColor); + canvas.drawCircle(coordinateX, coordinateY, radius, paint); + + paint.setColor(selectedColor); + canvas.drawRoundRect(rect, radius, radius, paint); + } +} diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/utils/CoordinatesUtils.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/utils/CoordinatesUtils.java new file mode 100644 index 000000000..8945b0b9a --- /dev/null +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/utils/CoordinatesUtils.java @@ -0,0 +1,195 @@ +package com.smarteist.autoimageslider.IndicatorView.utils; + +import android.util.Pair; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.smarteist.autoimageslider.IndicatorView.animation.type.IndicatorAnimationType; +import com.smarteist.autoimageslider.IndicatorView.draw.data.Indicator; +import com.smarteist.autoimageslider.IndicatorView.draw.data.Orientation; + +public class CoordinatesUtils { + + @SuppressWarnings("UnnecessaryLocalVariable") + public static int getCoordinate(@Nullable Indicator indicator, int position) { + if (indicator == null) { + return 0; + } + + if (indicator.getOrientation() == Orientation.HORIZONTAL) { + return getXCoordinate(indicator, position); + } else { + return getYCoordinate(indicator, position); + } + } + + @SuppressWarnings("UnnecessaryLocalVariable") + public static int getXCoordinate(@Nullable Indicator indicator, int position) { + if (indicator == null) { + return 0; + } + + int coordinate; + if (indicator.getOrientation() == Orientation.HORIZONTAL) { + coordinate = getHorizontalCoordinate(indicator, position); + } else { + coordinate = getVerticalCoordinate(indicator); + } + + coordinate += indicator.getPaddingLeft(); + return coordinate; + } + + public static int getYCoordinate(@Nullable Indicator indicator, int position) { + if (indicator == null) { + return 0; + } + + int coordinate; + if (indicator.getOrientation() == Orientation.HORIZONTAL) { + coordinate = getVerticalCoordinate(indicator); + } else { + coordinate = getHorizontalCoordinate(indicator, position); + } + + coordinate += indicator.getPaddingTop(); + return coordinate; + } + + @SuppressWarnings("SuspiciousNameCombination") + public static int getPosition(@Nullable Indicator indicator, float x, float y) { + if (indicator == null) { + return -1; + } + + float lengthCoordinate; + float heightCoordinate; + + if (indicator.getOrientation() == Orientation.HORIZONTAL) { + lengthCoordinate = x; + heightCoordinate = y; + } else { + lengthCoordinate = y; + heightCoordinate = x; + } + + return getFitPosition(indicator, lengthCoordinate, heightCoordinate); + } + + private static int getFitPosition(@NonNull Indicator indicator, float lengthCoordinate, float heightCoordinate) { + int count = indicator.getCount(); + int radius = indicator.getRadius(); + int stroke = indicator.getStroke(); + int padding = indicator.getPadding(); + + int height = indicator.getOrientation() == Orientation.HORIZONTAL ? indicator.getHeight() : indicator.getWidth(); + int length = 0; + + for (int i = 0; i < count; i++) { + int indicatorPadding = i > 0 ? padding : padding / 2; + int startValue = length; + + length += radius * 2 + (stroke / 2) + indicatorPadding; + int endValue = length; + + boolean fitLength = lengthCoordinate >= startValue && lengthCoordinate <= endValue; + boolean fitHeight = heightCoordinate >= 0 && heightCoordinate <= height; + + if (fitLength && fitHeight) { + return i; + } + } + + return -1; + } + + private static int getHorizontalCoordinate(@NonNull Indicator indicator, int position) { + int count = indicator.getCount(); + int radius = indicator.getRadius(); + int stroke = indicator.getStroke(); + int padding = indicator.getPadding(); + + int coordinate = 0; + for (int i = 0; i < count; i++) { + coordinate += radius + (stroke / 2); + + if (position == i) { + return coordinate; + } + + coordinate += radius + padding + (stroke / 2); + } + + if (indicator.getAnimationType() == IndicatorAnimationType.DROP) { + coordinate += radius * 2; + } + + return coordinate; + } + + private static int getVerticalCoordinate(@NonNull Indicator indicator) { + int radius = indicator.getRadius(); + int coordinate; + + if (indicator.getAnimationType() == IndicatorAnimationType.DROP) { + coordinate = radius * 3; + } else { + coordinate = radius; + } + + return coordinate; + } + + public static Pair getProgress(@NonNull Indicator indicator, int position, float positionOffset, boolean isRtl) { + int count = indicator.getCount(); + int selectedPosition = indicator.getSelectedPosition(); + + if (isRtl) { + position = (count - 1) - position; + } + + if (position < 0) { + position = 0; + + } else if (position > count - 1) { + position = count - 1; + } + + boolean isRightOverScrolled = position > selectedPosition; + boolean isLeftOverScrolled; + + if (isRtl) { + isLeftOverScrolled = position - 1 < selectedPosition; + } else { + isLeftOverScrolled = position + 1 < selectedPosition; + } + + if (isRightOverScrolled || isLeftOverScrolled) { + selectedPosition = position; + indicator.setSelectedPosition(selectedPosition); + } + + boolean slideToRightSide = selectedPosition == position && positionOffset != 0; + int selectingPosition; + float selectingProgress; + + if (slideToRightSide) { + selectingPosition = isRtl ? position - 1 : position + 1; + selectingProgress = positionOffset; + + } else { + selectingPosition = position; + selectingProgress = 1 - positionOffset; + } + + if (selectingProgress > 1) { + selectingProgress = 1; + + } else if (selectingProgress < 0) { + selectingProgress = 0; + } + + return new Pair<>(selectingPosition, selectingProgress); + } +} diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/utils/DensityUtils.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/utils/DensityUtils.java new file mode 100644 index 000000000..62fef4c60 --- /dev/null +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/utils/DensityUtils.java @@ -0,0 +1,15 @@ +package com.smarteist.autoimageslider.IndicatorView.utils; + +import android.content.res.Resources; +import android.util.TypedValue; + +public class DensityUtils { + + public static int dpToPx(int dp) { + return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, Resources.getSystem().getDisplayMetrics()); + } + + public static int pxToDp(float px) { + return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, px, Resources.getSystem().getDisplayMetrics()); + } +} diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/utils/IdUtils.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/utils/IdUtils.java new file mode 100644 index 000000000..3d31c948c --- /dev/null +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/IndicatorView/utils/IdUtils.java @@ -0,0 +1,37 @@ +package com.smarteist.autoimageslider.IndicatorView.utils; + +import android.os.Build; +import android.view.View; + +import java.util.concurrent.atomic.AtomicInteger; + +public class IdUtils { + + private static final AtomicInteger nextGeneratedId = new AtomicInteger(1); + + public static int generateViewId() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) { + return generateId(); + } else { + return View.generateViewId(); + } + } + + /** + * Generate a value suitable for use in #setId(int). + * This value will not collide with ID values generated at build time by aapt for R.id. + * + * @return a generated ID value + */ + private static int generateId() { + for (; ; ) { + final int result = nextGeneratedId.get(); + // aapt-generated IDs have the high byte nonzero; clamp to the range under that. + int newValue = result + 1; + if (newValue > 0x00FFFFFF) newValue = 1; // Roll over to 1, not 0. + if (nextGeneratedId.compareAndSet(result, newValue)) { + return result; + } + } + } +} diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/InfiniteAdapter/InfinitePagerAdapter.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/InfiniteAdapter/InfinitePagerAdapter.java new file mode 100644 index 000000000..c21a2acbe --- /dev/null +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/InfiniteAdapter/InfinitePagerAdapter.java @@ -0,0 +1,156 @@ +package com.smarteist.autoimageslider.InfiniteAdapter; + +import android.database.DataSetObserver; +import android.os.Parcelable; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.viewpager.widget.PagerAdapter; + +import com.smarteist.autoimageslider.SliderViewAdapter; + + +/** + * Its just a wrapper adapter class for providing infinite behavior + * for slider. + */ +public class InfinitePagerAdapter extends PagerAdapter { + + // Warning: it should be an even number. + public static final int INFINITE_SCROLL_LIMIT = 32400; + private static final String TAG = "InfinitePagerAdapter"; + private final SliderViewAdapter adapter; + + public InfinitePagerAdapter(SliderViewAdapter adapter) { + this.adapter = adapter; + } + + public PagerAdapter getRealAdapter() { + return this.adapter; + } + + @Override + public int getCount() { + if (getRealCount() < 1) { + return 0; + } + // warning: infinite scroller actually is not infinite! + // very big number will be cause memory problems. + return getRealCount() * INFINITE_SCROLL_LIMIT; + } + + /** + * @return the {@link #getCount()} result of the wrapped adapter + */ + public int getRealCount() { + try { + return getRealAdapter().getCount(); + } catch (Exception e) { + return 0; + } + } + + + /** + * @param item real position of item + * @return virtual mid point + */ + public int getMiddlePosition(int item) { + int midpoint = Math.max(0, getRealCount()) * (InfinitePagerAdapter.INFINITE_SCROLL_LIMIT / 2); + return item + midpoint; + } + + @NonNull + @Override + public Object instantiateItem(ViewGroup container, int virtualPosition) { + // prevent division by zer + if (getRealCount() < 1) { + return adapter.instantiateItem(container, 0); + } + //Log.i(TAG, "instantiateItem: real virtualPosition: " + virtualPosition); + //Log.i(TAG, "instantiateItem: virtual virtualPosition: " + virtualPosition); + + // only expose virtual virtualPosition to the inner adapter + return adapter.instantiateItem(container, getRealPosition(virtualPosition)); + } + + @Override + public void destroyItem(ViewGroup container, int virtualPosition, Object object) { + // prevent division by zero + if (getRealCount() < 1) { + adapter.destroyItem(container, 0, object); + return; + } + //Log.i(TAG, "destroyItem: real position: " + position); + //Log.i(TAG, "destroyItem: virtual position: " + virtualPosition); + + // only expose virtual position to the inner adapter + adapter.destroyItem(container, getRealPosition(virtualPosition), object); + } + + @Override + public void startUpdate(ViewGroup container) { + adapter.startUpdate(container); + } + + /* + * Delegate rest of methods directly to the inner adapter. + */ + @Override + public void finishUpdate(ViewGroup container) { + adapter.finishUpdate(container); + } + + @Override + public boolean isViewFromObject(View view, Object object) { + return adapter.isViewFromObject(view, object); + } + + @Override + public void restoreState(Parcelable bundle, ClassLoader classLoader) { + adapter.restoreState(bundle, classLoader); + } + + @Override + public Parcelable saveState() { + return adapter.saveState(); + } + + @Override + public CharSequence getPageTitle(int virtualPosition) { + return adapter.getPageTitle(getRealPosition(virtualPosition)); + } + + @Override + public float getPageWidth(int position) { + return adapter.getPageWidth(position); + } + + @Override + public void setPrimaryItem(ViewGroup container, int position, Object object) { + adapter.setPrimaryItem(container, position, object); + } + + @Override + public void unregisterDataSetObserver(DataSetObserver observer) { + adapter.unregisterDataSetObserver(observer); + } + + @Override + public void registerDataSetObserver(DataSetObserver observer) { + adapter.registerDataSetObserver(observer); + } + + @Override + public int getItemPosition(Object object) { + return adapter.getItemPosition(object); + } + + public int getRealPosition(int virtualPosition) { + if (getRealCount() > 0) { + return virtualPosition % getRealCount(); + } + return 0; + } +} \ No newline at end of file diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/SliderAnimations.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/SliderAnimations.java new file mode 100644 index 000000000..89db0f17e --- /dev/null +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/SliderAnimations.java @@ -0,0 +1,26 @@ +package com.smarteist.autoimageslider; + +public enum SliderAnimations { + ANTICLOCKSPINTRANSFORMATION, + CLOCK_SPINTRANSFORMATION, + CUBEINDEPTHTRANSFORMATION, + CUBEINROTATIONTRANSFORMATION, + CUBEINSCALINGTRANSFORMATION, + CUBEOUTDEPTHTRANSFORMATION, + CUBEOUTROTATIONTRANSFORMATION, + CUBEOUTSCALINGTRANSFORMATION, + DEPTHTRANSFORMATION, + FADETRANSFORMATION, + FANTRANSFORMATION, + FIDGETSPINTRANSFORMATION, + GATETRANSFORMATION, + HINGETRANSFORMATION, + HORIZONTALFLIPTRANSFORMATION, + POPTRANSFORMATION, + SIMPLETRANSFORMATION, + SPINNERTRANSFORMATION, + TOSSTRANSFORMATION, + VERTICALFLIPTRANSFORMATION, + VERTICALSHUTTRANSFORMATION, + ZOOMOUTTRANSFORMATION +} diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/SliderPager.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/SliderPager.java new file mode 100644 index 000000000..6fd30cf2f --- /dev/null +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/SliderPager.java @@ -0,0 +1,3127 @@ +package com.smarteist.autoimageslider; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.database.DataSetObserver; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.SystemClock; +import android.util.AttributeSet; +import android.view.FocusFinder; +import android.view.Gravity; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.SoundEffectConstants; +import android.view.VelocityTracker; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.view.accessibility.AccessibilityEvent; +import android.view.animation.Interpolator; +import android.widget.EdgeEffect; +import android.widget.Scroller; + +import androidx.annotation.CallSuper; +import androidx.annotation.DrawableRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.Px; +import androidx.core.content.ContextCompat; +import androidx.core.view.AccessibilityDelegateCompat; +import androidx.core.view.ViewCompat; +import androidx.core.view.WindowInsetsCompat; +import androidx.core.view.accessibility.AccessibilityEventCompat; +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; +import androidx.customview.view.AbsSavedState; +import androidx.viewpager.widget.PagerAdapter; + +import com.smarteist.autoimageslider.InfiniteAdapter.InfinitePagerAdapter; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +public class SliderPager extends ViewGroup { + + public static final int DEFAULT_SCROLL_DURATION = 250; + /** + * Indicates that the pager is in an idle, settled state. The current page + * is fully in view and no animation is in progress. + */ + public static final int SCROLL_STATE_IDLE = 0; + /** + * Indicates that the pager is currently being dragged by the user. + */ + public static final int SCROLL_STATE_DRAGGING = 1; + /** + * Indicates that the pager is in the process of settling to a final position. + */ + public static final int SCROLL_STATE_SETTLING = 2; + static final int[] LAYOUT_ATTRS = new int[]{ + android.R.attr.layout_gravity + }; + private static final String TAG = "SliderPager"; + private static final boolean DEBUG = false; + private static final boolean USE_CACHE = false; + private static final int DEFAULT_OFFSCREEN_PAGES = 1; + private static final int MAX_SETTLE_DURATION = 600; // ms + private static final int MIN_DISTANCE_FOR_FLING = 25; // dips + private static final int DEFAULT_GUTTER_SIZE = 16; // dips + private static final int MIN_FLING_VELOCITY = 400; // dips + private static final Comparator COMPARATOR = new Comparator() { + @Override + public int compare(ItemInfo lhs, ItemInfo rhs) { + return lhs.position - rhs.position; + } + }; + private static final Interpolator sInterpolator = new Interpolator() { + @Override + public float getInterpolation(float t) { + t -= 1.0f; + return t * t * t * t * t + 1.0f; + } + }; + /** + * Sentinel value for no current active pointer. + * Used by {@link #mActivePointerId}. + */ + private static final int INVALID_POINTER = -1; + // If the pager is at least this close to its final position, complete the scroll + // on touch down and let the user interact with the content inside instead of + // "catching" the flinging pager. + private static final int CLOSE_ENOUGH = 2; // dp + private static final int DRAW_ORDER_DEFAULT = 0; + private static final int DRAW_ORDER_FORWARD = 1; + private static final int DRAW_ORDER_REVERSE = 2; + private static final ViewPositionComparator sPositionComparator = new ViewPositionComparator(); + private final ArrayList mItems = new ArrayList<>(); + private final ItemInfo mTempItem = new ItemInfo(); + private final Rect mTempRect = new Rect(); + PagerAdapter mAdapter; + int mCurItem; // Index of currently displayed page. + /** + * Used to track what the expected number of items in the adapter should be. + * If the app changes this when we don't expect it, we'll throw a big obnoxious exception. + */ + private int mExpectedAdapterCount; + private int mRestoredCurItem = -1; + private Parcelable mRestoredAdapterState = null; + private ClassLoader mRestoredClassLoader = null; + private Scroller mScroller; + private boolean mIsScrollStarted; + private PagerObserver mObserver; + private int mPageMargin; + private Drawable mMarginDrawable; + private int mTopPageBounds; + private int mBottomPageBounds; + // Offsets of the first and last items, if known. + // Set during population, used to determine if we are at the beginning + // or end of the pager data set during touch scrolling. + private float mFirstOffset = -Float.MAX_VALUE; + private float mLastOffset = Float.MAX_VALUE; + private int mChildWidthMeasureSpec; + private int mChildHeightMeasureSpec; + private boolean mInLayout; + private boolean mScrollingCacheEnabled; + private boolean mPopulatePending; + private int mOffscreenPageLimit = DEFAULT_OFFSCREEN_PAGES; + private boolean mIsBeingDragged; + private boolean mIsUnableToDrag; + private int mDefaultGutterSize; + private int mGutterSize; + private int mTouchSlop; + /** + * Position of the last motion event. + */ + private float mLastMotionX; + private float mLastMotionY; + private float mInitialMotionX; + private float mInitialMotionY; + /** + * ID of the active pointer. This is used to retain consistency during + * drags/flings if multiple pointers are used. + */ + private int mActivePointerId = INVALID_POINTER; + /** + * Determines speed during touch scrolling + */ + private VelocityTracker mVelocityTracker; + private int mMinimumVelocity; + private int mMaximumVelocity; + private int mFlingDistance; + private int mCloseEnough; + private boolean mFakeDragging; + private long mFakeDragBeginTime; + private EdgeEffect mLeftEdge; + private EdgeEffect mRightEdge; + private boolean mFirstLayout = true; + private boolean mNeedCalculatePageOffsets = false; + private boolean mCalledSuper; + private int mDecorChildCount; + private List mOnPageChangeListeners; + private OnPageChangeListener mOnPageChangeListener; + private OnPageChangeListener mInternalPageChangeListener; + private List mAdapterChangeListeners; + private PageTransformer mPageTransformer; + private int mPageTransformerLayerType; + private int mDrawingOrder; + private ArrayList mDrawingOrderedChildren; + private int mScrollState = SCROLL_STATE_IDLE; + private final Runnable mEndScrollRunnable = new Runnable() { + @Override + public void run() { + setScrollState(SCROLL_STATE_IDLE); + populate(); + } + }; + + public SliderPager(@NonNull Context context) { + super(context); + initSliderPager(); + } + + public SliderPager(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + initSliderPager(); + } + + private static boolean isDecorView(@NonNull View view) { + Class clazz = view.getClass(); + return clazz.getAnnotation(DecorView.class) != null; + } + + void initSliderPager() { + setWillNotDraw(false); + setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); + setFocusable(true); + final Context context = getContext(); + mScroller = new OwnScroller(context, DEFAULT_SCROLL_DURATION, sInterpolator); + final ViewConfiguration configuration = ViewConfiguration.get(context); + final float density = context.getResources().getDisplayMetrics().density; + + mTouchSlop = configuration.getScaledPagingTouchSlop(); + mMinimumVelocity = (int) (MIN_FLING_VELOCITY * density); + mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); + mLeftEdge = new EdgeEffect(context); + mRightEdge = new EdgeEffect(context); + + mFlingDistance = (int) (MIN_DISTANCE_FOR_FLING * density); + mCloseEnough = (int) (CLOSE_ENOUGH * density); + mDefaultGutterSize = (int) (DEFAULT_GUTTER_SIZE * density); + + ViewCompat.setAccessibilityDelegate(this, new MyAccessibilityDelegate()); + + if (ViewCompat.getImportantForAccessibility(this) + == ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO) { + ViewCompat.setImportantForAccessibility(this, + ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES); + } + + ViewCompat.setOnApplyWindowInsetsListener(this, + new androidx.core.view.OnApplyWindowInsetsListener() { + private final Rect mTempRect = new Rect(); + + @Override + public WindowInsetsCompat onApplyWindowInsets(final View v, + final WindowInsetsCompat originalInsets) { + // First let the SliderPager itself try and consume them... + final WindowInsetsCompat applied = + ViewCompat.onApplyWindowInsets(v, originalInsets); + if (applied.isConsumed()) { + // If the SliderPager consumed all insets, return now + return applied; + } + + // Now we'll manually dispatch the insets to our children. Since SliderPager + // children are always full-height, we do not want to use the standard + // ViewGroup dispatchApplyWindowInsets since if child 0 consumes them, + // the rest of the children will not receive any insets. To workaround this + // we manually dispatch the applied insets, not allowing children to + // consume them from each other. We do however keep track of any insets + // which are consumed, returning the union of our children's consumption + final Rect res = mTempRect; + res.left = applied.getSystemWindowInsetLeft(); + res.top = applied.getSystemWindowInsetTop(); + res.right = applied.getSystemWindowInsetRight(); + res.bottom = applied.getSystemWindowInsetBottom(); + + for (int i = 0, count = getChildCount(); i < count; i++) { + final WindowInsetsCompat childInsets = ViewCompat + .dispatchApplyWindowInsets(getChildAt(i), applied); + // Now keep track of any consumed by tracking each dimension's min + // value + res.left = Math.min(childInsets.getSystemWindowInsetLeft(), + res.left); + res.top = Math.min(childInsets.getSystemWindowInsetTop(), + res.top); + res.right = Math.min(childInsets.getSystemWindowInsetRight(), + res.right); + res.bottom = Math.min(childInsets.getSystemWindowInsetBottom(), + res.bottom); + } + + // Now return a new WindowInsets, using the consumed window insets + return applied.replaceSystemWindowInsets( + res.left, res.top, res.right, res.bottom); + } + }); + } + + @Override + protected void onDetachedFromWindow() { + removeCallbacks(mEndScrollRunnable); + // To be on the safe side, abort the scroller + if ((mScroller != null) && !mScroller.isFinished()) { + mScroller.abortAnimation(); + } + super.onDetachedFromWindow(); + } + + void setScrollState(int newState) { + if (mScrollState == newState) { + return; + } + + mScrollState = newState; + if (mPageTransformer != null) { + // PageTransformers can do complex things that benefit from hardware layers. + enableLayers(newState != SCROLL_STATE_IDLE); + } + dispatchOnScrollStateChanged(newState); + } + + /** + * This method calls setViewPagerObserver defined in PagerAdapter by using + * reflection. + * + * @param observer pager observer + */ + private void setAdapterViewPagerObserver(PagerObserver observer) { + try { + Method setViewPagerObserver = PagerAdapter.class.getDeclaredMethod("setViewPagerObserver", DataSetObserver.class); + setViewPagerObserver.setAccessible(true); + setViewPagerObserver.invoke(mAdapter, observer); + } catch (Exception e) { + e.printStackTrace(); + } + } + + private void removeNonDecorViews() { + for (int i = 0; i < getChildCount(); i++) { + final View child = getChildAt(i); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (!lp.isDecor) { + removeViewAt(i); + i--; + } + } + } + + /** + * Retrieve the current adapter supplying pages. + * + * @return The currently registered PagerAdapter + */ + @Nullable + public PagerAdapter getAdapter() { + return mAdapter; + } + + /** + * Set a PagerAdapter that will supply views for this pager as needed. + * + * @param adapter Adapter to use + */ + public void setAdapter(PagerAdapter adapter) { + if (mAdapter != null) { + setAdapterViewPagerObserver(null); + mAdapter.startUpdate(this); + for (int i = 0; i < mItems.size(); i++) { + final ItemInfo ii = mItems.get(i); + mAdapter.destroyItem(this, ii.position, ii.object); + } + mAdapter.finishUpdate(this); + mItems.clear(); + removeNonDecorViews(); + mCurItem = 0; + scrollTo(0, 0); + } + + final PagerAdapter oldAdapter = mAdapter; + mAdapter = adapter; + mExpectedAdapterCount = 0; + + if (mAdapter != null) { + if (mObserver == null) { + mObserver = new PagerObserver(); + } + setAdapterViewPagerObserver(mObserver); + try { + mAdapter.registerDataSetObserver(mObserver); + } catch (Exception ignored) { + // maybe there is a registered observer + } + mPopulatePending = false; + final boolean wasFirstLayout = mFirstLayout; + mFirstLayout = true; + mExpectedAdapterCount = mAdapter.getCount(); + if (mRestoredCurItem >= 0) { + mAdapter.restoreState(mRestoredAdapterState, mRestoredClassLoader); + setCurrentItemInternal(mRestoredCurItem, false, true); + mRestoredCurItem = -1; + mRestoredAdapterState = null; + mRestoredClassLoader = null; + } else if (!wasFirstLayout) { + populate(); + } else { + requestLayout(); + } + } + + // Dispatch the change to any listeners + if (mAdapterChangeListeners != null && !mAdapterChangeListeners.isEmpty()) { + for (int i = 0, count = mAdapterChangeListeners.size(); i < count; i++) { + mAdapterChangeListeners.get(i).onAdapterChanged(this, oldAdapter, adapter); + } + } + } + + /** + * Add a listener that will be invoked whenever the adapter for this SliderPager changes. + * + * @param listener listener to add + */ + public void addOnAdapterChangeListener(@NonNull OnAdapterChangeListener listener) { + if (mAdapterChangeListeners == null) { + mAdapterChangeListeners = new ArrayList<>(); + } + mAdapterChangeListeners.add(listener); + } + + /** + * Remove a listener that was previously added via + * {@link #addOnAdapterChangeListener(OnAdapterChangeListener)}. + * + * @param listener listener to remove + */ + public void removeOnAdapterChangeListener(@NonNull OnAdapterChangeListener listener) { + if (mAdapterChangeListeners != null) { + mAdapterChangeListeners.remove(listener); + } + } + + private int getClientWidth() { + return getMeasuredWidth() - getPaddingLeft() - getPaddingRight(); + } + + /** + * Set the currently selected page. + * + * @param item Item index to select + * @param smoothScroll True to smoothly scroll to the new item, false to transition immediately + */ + public void setCurrentItem(int item, boolean smoothScroll) { + if (mAdapter instanceof InfinitePagerAdapter) { + item = ((InfinitePagerAdapter) mAdapter).getMiddlePosition(item); + } + mPopulatePending = false; + setCurrentItemInternal(item, smoothScroll, false); + } + + public int getCurrentItem() { + if (mAdapter instanceof InfinitePagerAdapter && ((InfinitePagerAdapter) mAdapter).getRealCount() > 0) { + return ((InfinitePagerAdapter) mAdapter).getRealPosition(mCurItem); + } + return mCurItem; + } + + /** + * Set the currently selected page. If the SliderPager has already been through its first + * layout with its current adapter there will be a smooth animated transition between + * the current item and the specified item. + * + * @param item Item index to select + */ + public void setCurrentItem(int item) { + mPopulatePending = false; + setCurrentItem(item, !mFirstLayout); + } + + void setCurrentItemInternal(int item, boolean smoothScroll, boolean always) { + setCurrentItemInternal(item, smoothScroll, always, 0); + } + + void setCurrentItemInternal(int item, boolean smoothScroll, boolean always, int velocity) { + if (mAdapter == null || mAdapter.getCount() <= 0) { + setScrollingCacheEnabled(false); + return; + } + if (!always && mCurItem == item && mItems.size() != 0) { + setScrollingCacheEnabled(false); + return; + } + + if (item < 0) { + item = 0; + } else if (item >= mAdapter.getCount()) { + item = mAdapter.getCount() - 1; + } + final int pageLimit = mOffscreenPageLimit; + if (item > (mCurItem + pageLimit) || item < (mCurItem - pageLimit)) { + // We are doing a jump by more than one page. To avoid + // glitches, we want to keep all current pages in the view + // until the scroll ends. + for (int i = 0; i < mItems.size(); i++) { + mItems.get(i).scrolling = true; + } + } + final boolean dispatchSelected = mCurItem != item; + + if (mFirstLayout) { + // We don't have any idea how big we are yet and shouldn't have any pages either. + // Just set things up and let the pending layout handle things. + mCurItem = item; + triggerOnPageChangeEvent(item); + requestLayout(); + } else { + populate(item); + scrollToItem(item, smoothScroll, velocity, dispatchSelected); + } + } + + private void scrollToItem(int item, boolean smoothScroll, int velocity, + boolean dispatchSelected) { + final ItemInfo curInfo = infoForPosition(item); + int destX = 0; + if (curInfo != null) { + final int width = getClientWidth(); + destX = (int) (width * Math.max(mFirstOffset, + Math.min(curInfo.offset, mLastOffset))); + } + if (smoothScroll) { + smoothScrollTo(destX, 0, velocity); + if (dispatchSelected) { + triggerOnPageChangeEvent(item); + } + } else { + if (dispatchSelected) { + triggerOnPageChangeEvent(item); + } + completeScroll(false); + scrollTo(destX, 0); + pageScrolled(destX); + } + } + + /** + * Set a listener that will be invoked whenever the page changes or is incrementally + * scrolled. See {@link ##OnPageChangeListener}. + * + * @param listener Listener to set + * @deprecated Use {@link #addOnPageChangeListener(OnPageChangeListener)} + * and {@link #removeOnPageChangeListener(OnPageChangeListener)} instead. + */ + @Deprecated + public void setOnPageChangeListener(OnPageChangeListener listener) { + mOnPageChangeListener = listener; + } + + /** + * Add a listener that will be invoked whenever the page changes or is incrementally + * scrolled. See {@link ##OnPageChangeListener}. + * + *

Components that add a listener should take care to remove it when finished. + * Other components that take ownership of a view may call {@link #clearOnPageChangeListeners()} + * to remove all attached listeners.

+ * + * @param listener listener to add + */ + public void addOnPageChangeListener(@NonNull OnPageChangeListener listener) { + if (mOnPageChangeListeners == null) { + mOnPageChangeListeners = new ArrayList<>(); + } + mOnPageChangeListeners.add(listener); + } + + /** + * Remove a listener that was previously added via + * {@link #addOnPageChangeListener(OnPageChangeListener)}. + * + * @param listener listener to remove + */ + public void removeOnPageChangeListener(@NonNull OnPageChangeListener listener) { + if (mOnPageChangeListeners != null) { + mOnPageChangeListeners.remove(listener); + } + } + + /** + * Remove all listeners that are notified of any changes in scroll state or position. + */ + public void clearOnPageChangeListeners() { + if (mOnPageChangeListeners != null) { + mOnPageChangeListeners.clear(); + } + } + + /** + * Sets a {@link ##PageTransformer} that will be called for each attached page whenever + * the scroll position is changed. This allows the application to apply custom property + * transformations to each page, overriding the default sliding behavior. + * + *

Note: By default, calling this method will cause contained pages to use + * {@link #View#LAYER_TYPE_HARDWARE}. This layer type allows custom alpha transformations, + * but it will cause issues if any of your pages contain a {@link ##android.view.SurfaceView} + * and you have not called {@link ##android.view.SurfaceView#setZOrderOnTop(boolean)} to put that + * {@link ##android.view.SurfaceView} above your app content. To disable this behavior, call + * {@link #setPageTransformer(boolean, PageTransformer, int)} and pass + * {@link #View#LAYER_TYPE_NONE} for {@code pageLayerType}.

+ * + * @param reverseDrawingOrder true if the supplied PageTransformer requires page views + * to be drawn from last to first instead of first to last. + * @param transformer PageTransformer that will modify each page's animation properties + */ + public void setPageTransformer(boolean reverseDrawingOrder, + @Nullable PageTransformer transformer) { + setPageTransformer(reverseDrawingOrder, transformer, View.LAYER_TYPE_HARDWARE); + } + + /** + * Sets a {@link ##PageTransformer} that will be called for each attached page whenever + * the scroll position is changed. This allows the application to apply custom property + * transformations to each page, overriding the default sliding behavior. + * + * @param reverseDrawingOrder true if the supplied PageTransformer requires page views + * to be drawn from last to first instead of first to last. + * @param transformer PageTransformer that will modify each page's animation properties + * @param pageLayerType View layer type that should be used for SliderPager pages. It should be + * either {@link #View#LAYER_TYPE_HARDWARE}, + * {@link #View#LAYER_TYPE_SOFTWARE}, or + * {@link #View#LAYER_TYPE_NONE}. + */ + public void setPageTransformer(boolean reverseDrawingOrder, + @Nullable PageTransformer transformer, int pageLayerType) { + final boolean hasTransformer = transformer != null; + final boolean needsPopulate = hasTransformer != (mPageTransformer != null); + mPageTransformer = transformer; + setChildrenDrawingOrderEnabled(hasTransformer); + if (hasTransformer) { + mDrawingOrder = reverseDrawingOrder ? DRAW_ORDER_REVERSE : DRAW_ORDER_FORWARD; + mPageTransformerLayerType = pageLayerType; + } else { + mDrawingOrder = DRAW_ORDER_DEFAULT; + } + if (needsPopulate) populate(); + } + + @Override + protected int getChildDrawingOrder(int childCount, int i) { + final int index = mDrawingOrder == DRAW_ORDER_REVERSE ? childCount - 1 - i : i; + + if (mDrawingOrderedChildren == null || mDrawingOrderedChildren.size() != getChildCount()) { + sortChildDrawingOrder(); + } + return ((LayoutParams) mDrawingOrderedChildren.get(index).getLayoutParams()).childIndex; + } + + /** + * Set a separate OnPageChangeListener for internal use by the support library. + * + * @param listener Listener to set + * @return The old listener that was set, if any. + */ + OnPageChangeListener setInternalPageChangeListener(OnPageChangeListener listener) { + OnPageChangeListener oldListener = mInternalPageChangeListener; + mInternalPageChangeListener = listener; + return oldListener; + } + + /** + * Returns the number of pages that will be retained to either side of the + * current page in the view hierarchy in an idle state. Defaults to 1. + * + * @return How many pages will be kept offscreen on either side + * @see #setOffscreenPageLimit(int) + */ + public int getOffscreenPageLimit() { + return mOffscreenPageLimit; + } + + /** + * Set the number of pages that should be retained to either side of the + * current page in the view hierarchy in an idle state. Pages beyond this + * limit will be recreated from the adapter when needed. + * + *

This is offered as an optimization. If you know in advance the number + * of pages you will need to support or have lazy-loading mechanisms in place + * on your pages, tweaking this setting can have benefits in perceived smoothness + * of paging animations and interaction. If you have a small number of pages (3-4) + * that you can keep active all at once, less time will be spent in layout for + * newly created view subtrees as the user pages back and forth.

+ * + *

You should keep this limit low, especially if your pages have complex layouts. + * This setting defaults to 1.

+ * + * @param limit How many pages will be kept offscreen in an idle state. + */ + public void setOffscreenPageLimit(int limit) { + if (limit < DEFAULT_OFFSCREEN_PAGES) { + limit = DEFAULT_OFFSCREEN_PAGES; + } + if (limit != mOffscreenPageLimit) { + mOffscreenPageLimit = limit; + populate(); + } + } + + /** + * Return the margin between pages. + * + * @return The size of the margin in pixels + */ + public int getPageMargin() { + return mPageMargin; + } + + /** + * Set the margin between pages. + * + * @param marginPixels Distance between adjacent pages in pixels + * @see #getPageMargin() + * @see #setPageMarginDrawable(Drawable) + * @see #setPageMarginDrawable(int) + */ + public void setPageMargin(int marginPixels) { + final int oldMargin = mPageMargin; + mPageMargin = marginPixels; + + final int width = getWidth(); + recomputeScrollPosition(width, width, marginPixels, oldMargin); + + requestLayout(); + } + + /** + * Set a drawable that will be used to fill the margin between pages. + * + * @param d Drawable to display between pages + */ + public void setPageMarginDrawable(@Nullable Drawable d) { + mMarginDrawable = d; + if (d != null) refreshDrawableState(); + setWillNotDraw(d == null); + invalidate(); + } + + /** + * Set a drawable that will be used to fill the margin between pages. + * + * @param resId Resource ID of a drawable to display between pages + */ + public void setPageMarginDrawable(@DrawableRes int resId) { + setPageMarginDrawable(ContextCompat.getDrawable(getContext(), resId)); + } + + @Override + protected boolean verifyDrawable(Drawable who) { + return super.verifyDrawable(who) || who == mMarginDrawable; + } + + public void setScrollDuration(int millis, Interpolator interpolator) { + if (interpolator != null) { + mScroller = new OwnScroller(getContext(), millis, interpolator); + } else { + mScroller = new OwnScroller(getContext(), millis); + } + } + + public void setScrollDuration(int millis) { + setScrollDuration(millis, null); + } + + @Override + protected void drawableStateChanged() { + super.drawableStateChanged(); + final Drawable d = mMarginDrawable; + if (d != null && d.isStateful()) { + d.setState(getDrawableState()); + } + } + + // We want the duration of the page snap animation to be influenced by the distance that + // the screen has to travel, however, we don't want this duration to be effected in a + // purely linear fashion. Instead, we use this method to moderate the effect that the distance + // of travel has on the overall snap duration. + float distanceInfluenceForSnapDuration(float f) { + f -= 0.5f; // center the values about 0. + f *= 0.3f * (float) Math.PI / 2.0f; + return (float) Math.sin(f); + } + + /** + * Like {@link #View#scrollBy}, but scroll smoothly instead of immediately. + * + * @param x the number of pixels to scroll by on the X axis + * @param y the number of pixels to scroll by on the Y axis + */ + void smoothScrollTo(int x, int y) { + smoothScrollTo(x, y, 0); + } + + /** + * Like {@link #View#scrollBy}, but scroll smoothly instead of immediately. + * + * @param x the number of pixels to scroll by on the X axis + * @param y the number of pixels to scroll by on the Y axis + * @param velocity the velocity associated with a fling, if applicable. (0 otherwise) + */ + void smoothScrollTo(int x, int y, int velocity) { + if (getChildCount() == 0) { + // Nothing to do. + setScrollingCacheEnabled(false); + return; + } + + int sx; + boolean wasScrolling = (mScroller != null) && !mScroller.isFinished(); + if (wasScrolling) { + // We're in the middle of a previously initiated scrolling. Check to see + // whether that scrolling has actually started (if we always call getStartX + // we can get a stale value from the scroller if it hadn't yet had its first + // computeScrollOffset call) to decide what is the current scrolling position. + sx = mIsScrollStarted ? mScroller.getCurrX() : mScroller.getStartX(); + // And abort the current scrolling. + mScroller.abortAnimation(); + setScrollingCacheEnabled(false); + } else { + sx = getScrollX(); + } + int sy = getScrollY(); + int dx = x - sx; + int dy = y - sy; + if (dx == 0 && dy == 0) { + completeScroll(false); + populate(); + setScrollState(SCROLL_STATE_IDLE); + return; + } + + setScrollingCacheEnabled(true); + setScrollState(SCROLL_STATE_SETTLING); + + final int width = getClientWidth(); + final int halfWidth = width / 2; + final float distanceRatio = Math.min(1f, 1.0f * Math.abs(dx) / width); + final float distance = halfWidth + halfWidth + * distanceInfluenceForSnapDuration(distanceRatio); + + int duration; + velocity = Math.abs(velocity); + if (velocity > 0) { + duration = 4 * Math.round(1000 * Math.abs(distance / velocity)); + } else { + final float pageWidth = width * mAdapter.getPageWidth(mCurItem); + final float pageDelta = (float) Math.abs(dx) / (pageWidth + mPageMargin); + duration = (int) ((pageDelta + 1) * 100); + } + duration = Math.min(duration, MAX_SETTLE_DURATION); + + // Reset the "scroll started" flag. It will be flipped to true in all places + // where we call computeScrollOffset(). + mIsScrollStarted = false; + mScroller.startScroll(sx, sy, dx, dy, duration); + ViewCompat.postInvalidateOnAnimation(this); + } + + ItemInfo addNewItem(int position, int index) { + ItemInfo ii = new ItemInfo(); + ii.position = position; + ii.object = mAdapter.instantiateItem(this, position); + ii.widthFactor = mAdapter.getPageWidth(position); + if (index < 0 || index >= mItems.size()) { + mItems.add(ii); + } else { + mItems.add(index, ii); + } + return ii; + } + + void dataSetChanged() { + // This method only gets called if our observer is attached, so mAdapter is non-null. + + final int adapterCount = mAdapter.getCount(); + mExpectedAdapterCount = adapterCount; + boolean needPopulate = mItems.size() < mOffscreenPageLimit * 2 + 1 + && mItems.size() < adapterCount; + int newCurrItem = mCurItem; + + boolean isUpdating = false; + for (int i = 0; i < mItems.size(); i++) { + final ItemInfo ii = mItems.get(i); + final int newPos = mAdapter.getItemPosition(ii.object); + + if (newPos == PagerAdapter.POSITION_UNCHANGED) { + continue; + } + + if (newPos == PagerAdapter.POSITION_NONE) { + mItems.remove(i); + i--; + + if (!isUpdating) { + mAdapter.startUpdate(this); + isUpdating = true; + } + + mAdapter.destroyItem(this, ii.position, ii.object); + needPopulate = true; + + if (mCurItem == ii.position) { + // Keep the current item in the valid range + newCurrItem = Math.max(0, Math.min(mCurItem, adapterCount - 1)); + needPopulate = true; + } + continue; + } + + if (ii.position != newPos) { + if (ii.position == mCurItem) { + // Our current item changed position. Follow it. + newCurrItem = newPos; + } + + ii.position = newPos; + needPopulate = true; + } + } + + if (isUpdating) { + mAdapter.finishUpdate(this); + } + + Collections.sort(mItems, COMPARATOR); + + if (needPopulate) { + // Reset our known page widths; populate will recompute them. + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (!lp.isDecor) { + lp.widthFactor = 0.f; + } + } + + setCurrentItemInternal(newCurrItem, false, true); + requestLayout(); + } + } + + void populate() { + populate(mCurItem); + } + + void populate(int newCurrentItem) { + ItemInfo oldCurInfo = null; + if (mCurItem != newCurrentItem) { + oldCurInfo = infoForPosition(mCurItem); + mCurItem = newCurrentItem; + } + + if (mAdapter == null) { + sortChildDrawingOrder(); + return; + } + + // Bail now if we are waiting to populate. This is to hold off + // on creating views from the time the user releases their finger to + // fling to a new position until we have finished the scroll to + // that position, avoiding glitches from happening at that point. + if (mPopulatePending) { + sortChildDrawingOrder(); + return; + } + + // Also, don't populate until we are attached to a window. This is to + // avoid trying to populate before we have restored our view hierarchy + // state and conflicting with what is restored. + if (getWindowToken() == null) { + return; + } + + mAdapter.startUpdate(this); + + final int pageLimit = mOffscreenPageLimit; + final int startPos = Math.max(0, mCurItem - pageLimit); + final int N = mAdapter.getCount(); + final int endPos = Math.min(N - 1, mCurItem + pageLimit); + + if (N != mExpectedAdapterCount) { + String resName; + try { + resName = getResources().getResourceName(getId()); + } catch (Resources.NotFoundException e) { + resName = Integer.toHexString(getId()); + } + throw new IllegalStateException("The application's PagerAdapter changed the adapter's" + + " contents without calling PagerAdapter#notifyDataSetChanged!" + + " Expected adapter item count: " + mExpectedAdapterCount + ", found: " + N + + " Pager id: " + resName + + " Pager class: " + getClass() + + " Problematic adapter: " + mAdapter.getClass()); + } + + // Locate the currently focused item or add it if needed. + int curIndex = -1; + ItemInfo curItem = null; + for (curIndex = 0; curIndex < mItems.size(); curIndex++) { + final ItemInfo ii = mItems.get(curIndex); + if (ii.position >= mCurItem) { + if (ii.position == mCurItem) curItem = ii; + break; + } + } + + if (curItem == null && N > 0) { + curItem = addNewItem(mCurItem, curIndex); + } + + // Fill 3x the available width or up to the number of offscreen + // pages requested to either side, whichever is larger. + // If we have no current item we have no work to do. + if (curItem != null) { + float extraWidthLeft = 0.f; + int itemIndex = curIndex - 1; + ItemInfo ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; + final int clientWidth = getClientWidth(); + final float leftWidthNeeded = clientWidth <= 0 ? 0 : + 2.f - curItem.widthFactor + (float) getPaddingLeft() / (float) clientWidth; + for (int pos = mCurItem - 1; pos >= 0; pos--) { + if (extraWidthLeft >= leftWidthNeeded && pos < startPos) { + if (ii == null) { + break; + } + if (pos == ii.position && !ii.scrolling) { + mItems.remove(itemIndex); + mAdapter.destroyItem(this, pos, ii.object); + itemIndex--; + curIndex--; + ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; + } + } else if (ii != null && pos == ii.position) { + extraWidthLeft += ii.widthFactor; + itemIndex--; + ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; + } else { + ii = addNewItem(pos, itemIndex + 1); + extraWidthLeft += ii.widthFactor; + curIndex++; + ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; + } + } + + float extraWidthRight = curItem.widthFactor; + itemIndex = curIndex + 1; + if (extraWidthRight < 2.f) { + ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; + final float rightWidthNeeded = clientWidth <= 0 ? 0 : + (float) getPaddingRight() / (float) clientWidth + 2.f; + for (int pos = mCurItem + 1; pos < N; pos++) { + if (extraWidthRight >= rightWidthNeeded && pos > endPos) { + if (ii == null) { + break; + } + if (pos == ii.position && !ii.scrolling) { + mItems.remove(itemIndex); + mAdapter.destroyItem(this, pos, ii.object); + ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; + } + } else if (ii != null && pos == ii.position) { + extraWidthRight += ii.widthFactor; + itemIndex++; + ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; + } else { + ii = addNewItem(pos, itemIndex); + itemIndex++; + extraWidthRight += ii.widthFactor; + ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; + } + } + } + + calculatePageOffsets(curItem, curIndex, oldCurInfo); + + mAdapter.setPrimaryItem(this, mCurItem, curItem.object); + } + + + mAdapter.finishUpdate(this); + + // Check width measurement of current pages and drawing sort order. + // Update LayoutParams as needed. + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + lp.childIndex = i; + if (!lp.isDecor && lp.widthFactor == 0.f) { + // 0 means requery the adapter for this, it doesn't have a valid width. + final ItemInfo ii = infoForChild(child); + if (ii != null) { + lp.widthFactor = ii.widthFactor; + lp.position = ii.position; + } + } + } + sortChildDrawingOrder(); + + if (hasFocus()) { + View currentFocused = findFocus(); + ItemInfo ii = currentFocused != null ? infoForAnyChild(currentFocused) : null; + if (ii == null || ii.position != mCurItem) { + for (int i = 0; i < getChildCount(); i++) { + View child = getChildAt(i); + ii = infoForChild(child); + if (ii != null && ii.position == mCurItem) { + if (child.requestFocus(View.FOCUS_FORWARD)) { + break; + } + } + } + } + } + } + + private void sortChildDrawingOrder() { + if (mDrawingOrder != DRAW_ORDER_DEFAULT) { + if (mDrawingOrderedChildren == null) { + mDrawingOrderedChildren = new ArrayList(); + } else { + mDrawingOrderedChildren.clear(); + } + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + mDrawingOrderedChildren.add(child); + } + Collections.sort(mDrawingOrderedChildren, sPositionComparator); + } + } + + private void triggerOnPageChangeEvent(int position) { + for (OnPageChangeListener eachListener : mOnPageChangeListeners) { + if (eachListener != null) { + if (mAdapter instanceof InfinitePagerAdapter) { + int n = ((InfinitePagerAdapter) mAdapter).getRealPosition(position); + eachListener.onPageSelected(n); + } else { + eachListener.onPageSelected(position); + } + + } + } + if (mInternalPageChangeListener != null) { + mInternalPageChangeListener.onPageSelected(position); + } + } + + private void calculatePageOffsets(ItemInfo curItem, int curIndex, ItemInfo oldCurInfo) { + final int N = mAdapter.getCount(); + final int width = getClientWidth(); + final float marginOffset = width > 0 ? (float) mPageMargin / width : 0; + // Fix up offsets for later layout. + if (oldCurInfo != null) { + final int oldCurPosition = oldCurInfo.position; + // Base offsets off of oldCurInfo. + if (oldCurPosition < curItem.position) { + int itemIndex = 0; + ItemInfo ii = null; + float offset = oldCurInfo.offset + oldCurInfo.widthFactor + marginOffset; + for (int pos = oldCurPosition + 1; + pos <= curItem.position && itemIndex < mItems.size(); pos++) { + ii = mItems.get(itemIndex); + while (pos > ii.position && itemIndex < mItems.size() - 1) { + itemIndex++; + ii = mItems.get(itemIndex); + } + while (pos < ii.position) { + // We don't have an item populated for this, + // ask the adapter for an offset. + offset += mAdapter.getPageWidth(pos) + marginOffset; + pos++; + } + ii.offset = offset; + offset += ii.widthFactor + marginOffset; + } + } else if (oldCurPosition > curItem.position) { + int itemIndex = mItems.size() - 1; + ItemInfo ii = null; + float offset = oldCurInfo.offset; + for (int pos = oldCurPosition - 1; + pos >= curItem.position && itemIndex >= 0; pos--) { + ii = mItems.get(itemIndex); + while (pos < ii.position && itemIndex > 0) { + itemIndex--; + ii = mItems.get(itemIndex); + } + while (pos > ii.position) { + // We don't have an item populated for this, + // ask the adapter for an offset. + offset -= mAdapter.getPageWidth(pos) + marginOffset; + pos--; + } + offset -= ii.widthFactor + marginOffset; + ii.offset = offset; + } + } + } + + // Base all offsets off of curItem. + final int itemCount = mItems.size(); + float offset = curItem.offset; + int pos = curItem.position - 1; + mFirstOffset = curItem.position == 0 ? curItem.offset : -Float.MAX_VALUE; + mLastOffset = curItem.position == N - 1 + ? curItem.offset + curItem.widthFactor - 1 : Float.MAX_VALUE; + // Previous pages + for (int i = curIndex - 1; i >= 0; i--, pos--) { + final ItemInfo ii = mItems.get(i); + while (pos > ii.position) { + offset -= mAdapter.getPageWidth(pos--) + marginOffset; + } + offset -= ii.widthFactor + marginOffset; + ii.offset = offset; + if (ii.position == 0) mFirstOffset = offset; + } + offset = curItem.offset + curItem.widthFactor + marginOffset; + pos = curItem.position + 1; + // Next pages + for (int i = curIndex + 1; i < itemCount; i++, pos++) { + final ItemInfo ii = mItems.get(i); + while (pos < ii.position) { + offset += mAdapter.getPageWidth(pos++) + marginOffset; + } + if (ii.position == N - 1) { + mLastOffset = offset + ii.widthFactor - 1; + } + ii.offset = offset; + offset += ii.widthFactor + marginOffset; + } + + mNeedCalculatePageOffsets = false; + } + + @Override + public Parcelable onSaveInstanceState() { + Parcelable superState = super.onSaveInstanceState(); + SavedState ss = new SavedState(superState); + ss.position = mCurItem; + if (mAdapter != null) { + ss.adapterState = mAdapter.saveState(); + } + return ss; + } + + @Override + public void onRestoreInstanceState(Parcelable state) { + if (!(state instanceof SavedState)) { + super.onRestoreInstanceState(state); + return; + } + + SavedState ss = (SavedState) state; + super.onRestoreInstanceState(ss.getSuperState()); + + if (mAdapter != null) { + mAdapter.restoreState(ss.adapterState, ss.loader); + setCurrentItemInternal(ss.position, false, true); + } else { + mRestoredCurItem = ss.position; + mRestoredAdapterState = ss.adapterState; + mRestoredClassLoader = ss.loader; + } + } + + @Override + public void addView(View child, int index, ViewGroup.LayoutParams params) { + if (!checkLayoutParams(params)) { + params = generateLayoutParams(params); + } + final LayoutParams lp = (LayoutParams) params; + // Any views added via inflation should be classed as part of the decor + lp.isDecor |= isDecorView(child); + if (mInLayout) { + if (lp != null && lp.isDecor) { + throw new IllegalStateException("Cannot add pager decor view during layout"); + } + lp.needsMeasure = true; + addViewInLayout(child, index, params); + } else { + super.addView(child, index, params); + } + + if (USE_CACHE) { + if (child.getVisibility() != GONE) { + child.setDrawingCacheEnabled(mScrollingCacheEnabled); + } else { + child.setDrawingCacheEnabled(false); + } + } + } + + @Override + public void removeView(View view) { + if (mInLayout) { + removeViewInLayout(view); + } else { + super.removeView(view); + } + } + + ItemInfo infoForChild(View child) { + for (int i = 0; i < mItems.size(); i++) { + ItemInfo ii = mItems.get(i); + if (mAdapter.isViewFromObject(child, ii.object)) { + return ii; + } + } + return null; + } + + ItemInfo infoForAnyChild(View child) { + ViewParent parent; + while ((parent = child.getParent()) != this) { + if (parent == null || !(parent instanceof View)) { + return null; + } + child = (View) parent; + } + return infoForChild(child); + } + + ItemInfo infoForPosition(int position) { + for (int i = 0; i < mItems.size(); i++) { + ItemInfo ii = mItems.get(i); + if (ii.position == position) { + return ii; + } + } + return null; + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + mFirstLayout = true; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + // For simple implementation, our internal size is always 0. + // We depend on the container to specify the layout size of + // our view. We can't really know what it is since we will be + // adding and removing different arbitrary views and do not + // want the layout to change as this happens. + setMeasuredDimension(getDefaultSize(0, widthMeasureSpec), + getDefaultSize(0, heightMeasureSpec)); + + final int measuredWidth = getMeasuredWidth(); + final int maxGutterSize = measuredWidth / 10; + mGutterSize = Math.min(maxGutterSize, mDefaultGutterSize); + + // Children are just made to fill our space. + int childWidthSize = measuredWidth - getPaddingLeft() - getPaddingRight(); + int childHeightSize = getMeasuredHeight() - getPaddingTop() - getPaddingBottom(); + + /* + * Make sure all children have been properly measured. Decor views first. + * Right now we cheat and make this less complicated by assuming decor + * views won't intersect. We will pin to edges based on gravity. + */ + int size = getChildCount(); + for (int i = 0; i < size; ++i) { + final View child = getChildAt(i); + if (child.getVisibility() != GONE) { + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (lp != null && lp.isDecor) { + final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK; + final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK; + int widthMode = MeasureSpec.AT_MOST; + int heightMode = MeasureSpec.AT_MOST; + boolean consumeVertical = vgrav == Gravity.TOP || vgrav == Gravity.BOTTOM; + boolean consumeHorizontal = hgrav == Gravity.LEFT || hgrav == Gravity.RIGHT; + + if (consumeVertical) { + widthMode = MeasureSpec.EXACTLY; + } else if (consumeHorizontal) { + heightMode = MeasureSpec.EXACTLY; + } + + int widthSize = childWidthSize; + int heightSize = childHeightSize; + if (lp.width != LayoutParams.WRAP_CONTENT) { + widthMode = MeasureSpec.EXACTLY; + if (lp.width != LayoutParams.MATCH_PARENT) { + widthSize = lp.width; + } + } + if (lp.height != LayoutParams.WRAP_CONTENT) { + heightMode = MeasureSpec.EXACTLY; + if (lp.height != LayoutParams.MATCH_PARENT) { + heightSize = lp.height; + } + } + final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, widthMode); + final int heightSpec = MeasureSpec.makeMeasureSpec(heightSize, heightMode); + child.measure(widthSpec, heightSpec); + + if (consumeVertical) { + childHeightSize -= child.getMeasuredHeight(); + } else if (consumeHorizontal) { + childWidthSize -= child.getMeasuredWidth(); + } + } + } + } + + mChildWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidthSize, MeasureSpec.EXACTLY); + mChildHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeightSize, MeasureSpec.EXACTLY); + + // Make sure we have created all fragments that we need to have shown. + mInLayout = true; + populate(); + mInLayout = false; + + // Page views next. + size = getChildCount(); + for (int i = 0; i < size; ++i) { + final View child = getChildAt(i); + if (child.getVisibility() != GONE) { + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (lp == null || !lp.isDecor) { + final int widthSpec = MeasureSpec.makeMeasureSpec( + (int) (childWidthSize * lp.widthFactor), MeasureSpec.EXACTLY); + child.measure(widthSpec, mChildHeightMeasureSpec); + } + } + } + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + + // Make sure scroll position is set correctly. + if (w != oldw) { + recomputeScrollPosition(w, oldw, mPageMargin, mPageMargin); + } + } + + private void recomputeScrollPosition(int width, int oldWidth, int margin, int oldMargin) { + if (oldWidth > 0 && !mItems.isEmpty()) { + if (!mScroller.isFinished()) { + mScroller.setFinalX(getCurrentItem() * getClientWidth()); + } else { + final int widthWithMargin = width - getPaddingLeft() - getPaddingRight() + margin; + final int oldWidthWithMargin = oldWidth - getPaddingLeft() - getPaddingRight() + + oldMargin; + final int xpos = getScrollX(); + final float pageOffset = (float) xpos / oldWidthWithMargin; + final int newOffsetPixels = (int) (pageOffset * widthWithMargin); + + scrollTo(newOffsetPixels, getScrollY()); + } + } else { + final ItemInfo ii = infoForPosition(mCurItem); + final float scrollOffset = ii != null ? Math.min(ii.offset, mLastOffset) : 0; + final int scrollPos = + (int) (scrollOffset * (width - getPaddingLeft() - getPaddingRight())); + if (scrollPos != getScrollX()) { + completeScroll(false); + scrollTo(scrollPos, getScrollY()); + } + } + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + final int count = getChildCount(); + int width = r - l; + int height = b - t; + int paddingLeft = getPaddingLeft(); + int paddingTop = getPaddingTop(); + int paddingRight = getPaddingRight(); + int paddingBottom = getPaddingBottom(); + final int scrollX = getScrollX(); + + int decorCount = 0; + + // First pass - decor views. We need to do this in two passes so that + // we have the proper offsets for non-decor views later. + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + if (child.getVisibility() != GONE) { + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + int childLeft = 0; + int childTop = 0; + if (lp.isDecor) { + final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK; + final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK; + switch (hgrav) { + default: + childLeft = paddingLeft; + break; + case Gravity.LEFT: + childLeft = paddingLeft; + paddingLeft += child.getMeasuredWidth(); + break; + case Gravity.CENTER_HORIZONTAL: + childLeft = Math.max((width - child.getMeasuredWidth()) / 2, + paddingLeft); + break; + case Gravity.RIGHT: + childLeft = width - paddingRight - child.getMeasuredWidth(); + paddingRight += child.getMeasuredWidth(); + break; + } + switch (vgrav) { + default: + childTop = paddingTop; + break; + case Gravity.TOP: + childTop = paddingTop; + paddingTop += child.getMeasuredHeight(); + break; + case Gravity.CENTER_VERTICAL: + childTop = Math.max((height - child.getMeasuredHeight()) / 2, + paddingTop); + break; + case Gravity.BOTTOM: + childTop = height - paddingBottom - child.getMeasuredHeight(); + paddingBottom += child.getMeasuredHeight(); + break; + } + childLeft += scrollX; + child.layout(childLeft, childTop, + childLeft + child.getMeasuredWidth(), + childTop + child.getMeasuredHeight()); + decorCount++; + } + } + } + + final int childWidth = width - paddingLeft - paddingRight; + // Page views. Do this once we have the right padding offsets from above. + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + if (child.getVisibility() != GONE) { + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + ItemInfo ii; + if (!lp.isDecor && (ii = infoForChild(child)) != null) { + int loff = (int) (childWidth * ii.offset); + int childLeft = paddingLeft + loff; + int childTop = paddingTop; + if (lp.needsMeasure) { + // This was added during layout and needs measurement. + // Do it now that we know what we're working with. + lp.needsMeasure = false; + final int widthSpec = MeasureSpec.makeMeasureSpec( + (int) (childWidth * lp.widthFactor), + MeasureSpec.EXACTLY); + final int heightSpec = MeasureSpec.makeMeasureSpec( + height - paddingTop - paddingBottom, + MeasureSpec.EXACTLY); + child.measure(widthSpec, heightSpec); + } + child.layout(childLeft, childTop, + childLeft + child.getMeasuredWidth(), + childTop + child.getMeasuredHeight()); + } + } + } + mTopPageBounds = paddingTop; + mBottomPageBounds = height - paddingBottom; + mDecorChildCount = decorCount; + + if (mFirstLayout) { + scrollToItem(mCurItem, false, 0, false); + } + mFirstLayout = false; + } + + @Override + public void computeScroll() { + mIsScrollStarted = true; + if (!mScroller.isFinished() && mScroller.computeScrollOffset()) { + int oldX = getScrollX(); + int oldY = getScrollY(); + int x = mScroller.getCurrX(); + int y = mScroller.getCurrY(); + + if (oldX != x || oldY != y) { + scrollTo(x, y); + if (!pageScrolled(x)) { + mScroller.abortAnimation(); + scrollTo(0, y); + } + } + + // Keep on drawing until the animation has finished. + ViewCompat.postInvalidateOnAnimation(this); + return; + } + + // Done with scroll, clean up state. + completeScroll(true); + } + + private boolean pageScrolled(int xpos) { + if (mItems.size() == 0) { + if (mFirstLayout) { + // If we haven't been laid out yet, we probably just haven't been populated yet. + // Let's skip this call since it doesn't make sense in this state + return false; + } + mCalledSuper = false; + onPageScrolled(0, 0, 0); + if (!mCalledSuper) { + throw new IllegalStateException( + "onPageScrolled did not call superclass implementation"); + } + return false; + } + final ItemInfo ii = infoForCurrentScrollPosition(); + final int width = getClientWidth(); + final int widthWithMargin = width + mPageMargin; + final float marginOffset = (float) mPageMargin / width; + final int currentPage = ii.position; + final float pageOffset = (((float) xpos / width) - ii.offset) + / (ii.widthFactor + marginOffset); + final int offsetPixels = (int) (pageOffset * widthWithMargin); + + mCalledSuper = false; + onPageScrolled(currentPage, pageOffset, offsetPixels); + if (!mCalledSuper) { + throw new IllegalStateException( + "onPageScrolled did not call superclass implementation"); + } + return true; + } + + /** + * This method will be invoked when the current page is scrolled, either as part + * of a programmatically initiated smooth scroll or a user initiated touch scroll. + * If you override this method you must call through to the superclass implementation + * (e.g. super.onPageScrolled(position, offset, offsetPixels)) before onPageScrolled + * returns. + * + * @param position Position index of the first page currently being displayed. + * Page position+1 will be visible if positionOffset is nonzero. + * @param offset Value from [0, 1) indicating the offset from the page at position. + * @param offsetPixels Value in pixels indicating the offset from position. + */ + @CallSuper + protected void onPageScrolled(int position, float offset, int offsetPixels) { + // Offset any decor views if needed - keep them on-screen at all times. + if (mDecorChildCount > 0) { + final int scrollX = getScrollX(); + int paddingLeft = getPaddingLeft(); + int paddingRight = getPaddingRight(); + final int width = getWidth(); + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (!lp.isDecor) continue; + + final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK; + int childLeft = 0; + switch (hgrav) { + default: + childLeft = paddingLeft; + break; + case Gravity.LEFT: + childLeft = paddingLeft; + paddingLeft += child.getWidth(); + break; + case Gravity.CENTER_HORIZONTAL: + childLeft = Math.max((width - child.getMeasuredWidth()) / 2, + paddingLeft); + break; + case Gravity.RIGHT: + childLeft = width - paddingRight - child.getMeasuredWidth(); + paddingRight += child.getMeasuredWidth(); + break; + } + childLeft += scrollX; + + final int childOffset = childLeft - child.getLeft(); + if (childOffset != 0) { + child.offsetLeftAndRight(childOffset); + } + } + } + + dispatchOnPageScrolled(position, offset, offsetPixels); + + if (mPageTransformer != null) { + final int scrollX = getScrollX(); + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + + if (lp.isDecor) continue; + final float transformPos = (float) (child.getLeft() - scrollX) / getClientWidth(); + mPageTransformer.transformPage(child, transformPos); + } + } + + mCalledSuper = true; + } + + private void dispatchOnPageScrolled(int position, float offset, int offsetPixels) { + if (mOnPageChangeListener != null) { + mOnPageChangeListener.onPageScrolled(position, offset, offsetPixels); + } + if (mOnPageChangeListeners != null) { + for (int i = 0, z = mOnPageChangeListeners.size(); i < z; i++) { + OnPageChangeListener listener = mOnPageChangeListeners.get(i); + if (listener != null) { + listener.onPageScrolled(position, offset, offsetPixels); + } + } + } + if (mInternalPageChangeListener != null) { + mInternalPageChangeListener.onPageScrolled(position, offset, offsetPixels); + } + } + + private void dispatchOnPageSelected(int position) { + if (mOnPageChangeListener != null) { + mOnPageChangeListener.onPageSelected(position); + } + if (mOnPageChangeListeners != null) { + for (int i = 0, z = mOnPageChangeListeners.size(); i < z; i++) { + OnPageChangeListener listener = mOnPageChangeListeners.get(i); + if (listener != null) { + listener.onPageSelected(position); + } + } + } + if (mInternalPageChangeListener != null) { + mInternalPageChangeListener.onPageSelected(position); + } + } + + private void dispatchOnScrollStateChanged(int state) { + if (mOnPageChangeListener != null) { + mOnPageChangeListener.onPageScrollStateChanged(state); + } + if (mOnPageChangeListeners != null) { + for (int i = 0, z = mOnPageChangeListeners.size(); i < z; i++) { + OnPageChangeListener listener = mOnPageChangeListeners.get(i); + if (listener != null) { + listener.onPageScrollStateChanged(state); + } + } + } + if (mInternalPageChangeListener != null) { + mInternalPageChangeListener.onPageScrollStateChanged(state); + } + } + + private void completeScroll(boolean postEvents) { + boolean needPopulate = mScrollState == SCROLL_STATE_SETTLING; + if (needPopulate) { + // Done with scroll, no longer want to cache view drawing. + setScrollingCacheEnabled(false); + boolean wasScrolling = !mScroller.isFinished(); + if (wasScrolling) { + mScroller.abortAnimation(); + int oldX = getScrollX(); + int oldY = getScrollY(); + int x = mScroller.getCurrX(); + int y = mScroller.getCurrY(); + if (oldX != x || oldY != y) { + scrollTo(x, y); + if (x != oldX) { + pageScrolled(x); + } + } + } + } + mPopulatePending = false; + for (int i = 0; i < mItems.size(); i++) { + ItemInfo ii = mItems.get(i); + if (ii.scrolling) { + needPopulate = true; + ii.scrolling = false; + } + } + if (needPopulate) { + if (postEvents) { + ViewCompat.postOnAnimation(this, mEndScrollRunnable); + } else { + mEndScrollRunnable.run(); + } + } + } + + private boolean isGutterDrag(float x, float dx) { + return (x < mGutterSize && dx > 0) || (x > getWidth() - mGutterSize && dx < 0); + } + + private void enableLayers(boolean enable) { + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final int layerType = enable + ? mPageTransformerLayerType : View.LAYER_TYPE_NONE; + getChildAt(i).setLayerType(layerType, null); + } + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + /* + * This method JUST determines whether we want to intercept the motion. + * If we return true, onMotionEvent will be called and we do the actual + * scrolling there. + */ + try { + final int action = ev.getAction() & MotionEvent.ACTION_MASK; + + // Always take care of the touch gesture being complete. + if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { + // Release the drag. + resetTouch(); + return false; + } + + // Nothing more to do here if we have decided whether or not we + // are dragging. + if (action != MotionEvent.ACTION_DOWN) { + if (mIsBeingDragged) { + return true; + } + if (mIsUnableToDrag) { + return false; + } + } + + switch (action) { + case MotionEvent.ACTION_MOVE: { + /* + * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check + * whether the user has moved far enough from his original down touch. + */ + + /* + * Locally do absolute value. mLastMotionY is set to the y value + * of the down event. + */ + final int activePointerId = mActivePointerId; + if (activePointerId == INVALID_POINTER) { + // If we don't have a valid id, the touch down wasn't on content. + break; + } + + final int pointerIndex = ev.findPointerIndex(activePointerId); + final float x = ev.getX(pointerIndex); + final float dx = x - mLastMotionX; + final float xDiff = Math.abs(dx); + final float y = ev.getY(pointerIndex); + final float yDiff = Math.abs(y - mInitialMotionY); + + if (dx != 0 && !isGutterDrag(mLastMotionX, dx) + && canScroll(this, false, (int) dx, (int) x, (int) y)) { + // Nested view has scrollable area under this point. Let it be handled there. + mLastMotionX = x; + mLastMotionY = y; + mIsUnableToDrag = true; + return false; + } + if (xDiff > mTouchSlop && xDiff * 0.5f > yDiff) { + mIsBeingDragged = true; + requestParentDisallowInterceptTouchEvent(true); + setScrollState(SCROLL_STATE_DRAGGING); + mLastMotionX = dx > 0 + ? mInitialMotionX + mTouchSlop : mInitialMotionX - mTouchSlop; + mLastMotionY = y; + setScrollingCacheEnabled(true); + } else if (yDiff > mTouchSlop) { + // The finger has moved enough in the vertical + // direction to be counted as a drag... abort + // any attempt to drag horizontally, to work correctly + // with children that have scrolling containers. + mIsUnableToDrag = true; + } + if (mIsBeingDragged) { + // Scroll to follow the motion event + if (performDrag(x)) { + ViewCompat.postInvalidateOnAnimation(this); + } + } + break; + } + + case MotionEvent.ACTION_DOWN: { + /* + * Remember location of down touch. + * ACTION_DOWN always refers to pointer index 0. + */ + mLastMotionX = mInitialMotionX = ev.getX(); + mLastMotionY = mInitialMotionY = ev.getY(); + mActivePointerId = ev.getPointerId(0); + mIsUnableToDrag = false; + + mIsScrollStarted = true; + mScroller.computeScrollOffset(); + if (mScrollState == SCROLL_STATE_SETTLING + && Math.abs(mScroller.getFinalX() - mScroller.getCurrX()) > mCloseEnough) { + // Let the user 'catch' the pager as it animates. + mScroller.abortAnimation(); + mPopulatePending = false; + populate(); + mIsBeingDragged = true; + requestParentDisallowInterceptTouchEvent(true); + setScrollState(SCROLL_STATE_DRAGGING); + } else { + completeScroll(false); + mIsBeingDragged = false; + } + + break; + } + + case MotionEvent.ACTION_POINTER_UP: + onSecondaryPointerUp(ev); + break; + } + + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } + mVelocityTracker.addMovement(ev); + + /* + * The only time we want to intercept motion events is if we are in the + * drag mode. + */ + return mIsBeingDragged; + } catch (Exception e) { + e.printStackTrace(); + return false; + } + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + if (mFakeDragging) { + // A fake drag is in progress already, ignore this real one + // but still eat the touch events. + // (It is likely that the user is multi-touching the screen.) + return true; + } + + if (ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) { + // Don't handle edge touches immediately -- they may actually belong to one of our + // descendants. + return false; + } + + if (mAdapter == null || mAdapter.getCount() == 0) { + // Nothing to present or scroll; nothing to touch. + return false; + } + + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } + mVelocityTracker.addMovement(ev); + + final int action = ev.getAction(); + boolean needsInvalidate = false; + + switch (action & MotionEvent.ACTION_MASK) { + case MotionEvent.ACTION_DOWN: { + mScroller.abortAnimation(); + mPopulatePending = false; + populate(); + + // Remember where the motion event started + mLastMotionX = mInitialMotionX = ev.getX(); + mLastMotionY = mInitialMotionY = ev.getY(); + mActivePointerId = ev.getPointerId(0); + break; + } + case MotionEvent.ACTION_MOVE: + if (!mIsBeingDragged) { + final int pointerIndex = ev.findPointerIndex(mActivePointerId); + if (pointerIndex == -1) { + // A child has consumed some touch events and put us into an inconsistent + // state. + needsInvalidate = resetTouch(); + break; + } + final float x = ev.getX(pointerIndex); + final float xDiff = Math.abs(x - mLastMotionX); + final float y = ev.getY(pointerIndex); + final float yDiff = Math.abs(y - mLastMotionY); + + if (xDiff > mTouchSlop && xDiff > yDiff) { + mIsBeingDragged = true; + requestParentDisallowInterceptTouchEvent(true); + mLastMotionX = x - mInitialMotionX > 0 ? mInitialMotionX + mTouchSlop : + mInitialMotionX - mTouchSlop; + mLastMotionY = y; + setScrollState(SCROLL_STATE_DRAGGING); + setScrollingCacheEnabled(true); + + // Disallow Parent Intercept, just in case + ViewParent parent = getParent(); + if (parent != null) { + parent.requestDisallowInterceptTouchEvent(true); + } + } + } + // Not else! Note that mIsBeingDragged can be set above. + if (mIsBeingDragged) { + // Scroll to follow the motion event + final int activePointerIndex = ev.findPointerIndex(mActivePointerId); + final float x = ev.getX(activePointerIndex); + needsInvalidate |= performDrag(x); + } + break; + case MotionEvent.ACTION_UP: + if (mIsBeingDragged) { + final VelocityTracker velocityTracker = mVelocityTracker; + velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); + int initialVelocity = (int) velocityTracker.getXVelocity(mActivePointerId); + mPopulatePending = true; + final int width = getClientWidth(); + final int scrollX = getScrollX(); + final ItemInfo ii = infoForCurrentScrollPosition(); + final float marginOffset = (float) mPageMargin / width; + final int currentPage = ii.position; + final float pageOffset = (((float) scrollX / width) - ii.offset) + / (ii.widthFactor + marginOffset); + final int activePointerIndex = ev.findPointerIndex(mActivePointerId); + final float x = ev.getX(activePointerIndex); + final int totalDelta = (int) (x - mInitialMotionX); + int nextPage = determineTargetPage(currentPage, pageOffset, initialVelocity, + totalDelta); + setCurrentItemInternal(nextPage, true, true, initialVelocity); + + needsInvalidate = resetTouch(); + } + break; + case MotionEvent.ACTION_CANCEL: + if (mIsBeingDragged) { + scrollToItem(mCurItem, true, 0, false); + needsInvalidate = resetTouch(); + } + break; + case MotionEvent.ACTION_POINTER_DOWN: { + final int index = ev.getActionIndex(); + final float x = ev.getX(index); + mLastMotionX = x; + mActivePointerId = ev.getPointerId(index); + break; + } + case MotionEvent.ACTION_POINTER_UP: + onSecondaryPointerUp(ev); + mLastMotionX = ev.getX(ev.findPointerIndex(mActivePointerId)); + break; + } + if (needsInvalidate) { + ViewCompat.postInvalidateOnAnimation(this); + } + return true; + } + + private boolean resetTouch() { + boolean needsInvalidate; + mActivePointerId = INVALID_POINTER; + endDrag(); + mLeftEdge.onRelease(); + mRightEdge.onRelease(); + needsInvalidate = mLeftEdge.isFinished() || mRightEdge.isFinished(); + return needsInvalidate; + } + + private void requestParentDisallowInterceptTouchEvent(boolean disallowIntercept) { + final ViewParent parent = getParent(); + if (parent != null) { + parent.requestDisallowInterceptTouchEvent(disallowIntercept); + } + } + + private boolean performDrag(float x) { + boolean needsInvalidate = false; + + final float deltaX = mLastMotionX - x; + mLastMotionX = x; + + float oldScrollX = getScrollX(); + float scrollX = oldScrollX + deltaX; + final int width = getClientWidth(); + + float leftBound = width * mFirstOffset; + float rightBound = width * mLastOffset; + boolean leftAbsolute = true; + boolean rightAbsolute = true; + + final ItemInfo firstItem = mItems.get(0); + final ItemInfo lastItem = mItems.get(mItems.size() - 1); + if (firstItem.position != 0) { + leftAbsolute = false; + leftBound = firstItem.offset * width; + } + if (lastItem.position != mAdapter.getCount() - 1) { + rightAbsolute = false; + rightBound = lastItem.offset * width; + } + + if (scrollX < leftBound) { + if (leftAbsolute) { + float over = leftBound - scrollX; + mLeftEdge.onPull(Math.abs(over) / width); + needsInvalidate = true; + } + scrollX = leftBound; + } else if (scrollX > rightBound) { + if (rightAbsolute) { + float over = scrollX - rightBound; + mRightEdge.onPull(Math.abs(over) / width); + needsInvalidate = true; + } + scrollX = rightBound; + } + // Don't lose the rounded component + mLastMotionX += scrollX - (int) scrollX; + scrollTo((int) scrollX, getScrollY()); + pageScrolled((int) scrollX); + + return needsInvalidate; + } + + /** + * @return Info about the page at the current scroll position. + * This can be synthetic for a missing middle page; the 'object' field can be null. + */ + private ItemInfo infoForCurrentScrollPosition() { + final int width = getClientWidth(); + final float scrollOffset = width > 0 ? (float) getScrollX() / width : 0; + final float marginOffset = width > 0 ? (float) mPageMargin / width : 0; + int lastPos = -1; + float lastOffset = 0.f; + float lastWidth = 0.f; + boolean first = true; + + ItemInfo lastItem = null; + for (int i = 0; i < mItems.size(); i++) { + ItemInfo ii = mItems.get(i); + float offset; + if (!first && ii.position != lastPos + 1) { + // Create a synthetic item for a missing page. + ii = mTempItem; + ii.offset = lastOffset + lastWidth + marginOffset; + ii.position = lastPos + 1; + ii.widthFactor = mAdapter.getPageWidth(ii.position); + i--; + } + offset = ii.offset; + + final float leftBound = offset; + final float rightBound = offset + ii.widthFactor + marginOffset; + if (first || scrollOffset >= leftBound) { + if (scrollOffset < rightBound || i == mItems.size() - 1) { + return ii; + } + } else { + return lastItem; + } + first = false; + lastPos = ii.position; + lastOffset = offset; + lastWidth = ii.widthFactor; + lastItem = ii; + } + + return lastItem; + } + + private int determineTargetPage(int currentPage, float pageOffset, int velocity, int deltaX) { + int targetPage; + if (Math.abs(deltaX) > mFlingDistance && Math.abs(velocity) > mMinimumVelocity) { + targetPage = velocity > 0 ? currentPage : currentPage + 1; + } else { + final float truncator = currentPage >= mCurItem ? 0.4f : 0.6f; + targetPage = currentPage + (int) (pageOffset + truncator); + } + + if (mItems.size() > 0) { + final ItemInfo firstItem = mItems.get(0); + final ItemInfo lastItem = mItems.get(mItems.size() - 1); + + // Only let the user target pages we have items for + targetPage = Math.max(firstItem.position, Math.min(targetPage, lastItem.position)); + } + + return targetPage; + } + + @Override + public void draw(Canvas canvas) { + super.draw(canvas); + boolean needsInvalidate = false; + + final int overScrollMode = getOverScrollMode(); + if (overScrollMode == View.OVER_SCROLL_ALWAYS + || (overScrollMode == View.OVER_SCROLL_IF_CONTENT_SCROLLS + && mAdapter != null && mAdapter.getCount() > 1)) { + if (!mLeftEdge.isFinished()) { + final int restoreCount = canvas.save(); + final int height = getHeight() - getPaddingTop() - getPaddingBottom(); + final int width = getWidth(); + + canvas.rotate(270); + canvas.translate(-height + getPaddingTop(), mFirstOffset * width); + mLeftEdge.setSize(height, width); + needsInvalidate |= mLeftEdge.draw(canvas); + canvas.restoreToCount(restoreCount); + } + if (!mRightEdge.isFinished()) { + final int restoreCount = canvas.save(); + final int width = getWidth(); + final int height = getHeight() - getPaddingTop() - getPaddingBottom(); + + canvas.rotate(90); + canvas.translate(-getPaddingTop(), -(mLastOffset + 1) * width); + mRightEdge.setSize(height, width); + needsInvalidate |= mRightEdge.draw(canvas); + canvas.restoreToCount(restoreCount); + } + } else { + mLeftEdge.finish(); + mRightEdge.finish(); + } + + if (needsInvalidate) { + // Keep animating + ViewCompat.postInvalidateOnAnimation(this); + } + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + // Draw the margin drawable between pages if needed. + if (mPageMargin > 0 && mMarginDrawable != null && mItems.size() > 0 && mAdapter != null) { + final int scrollX = getScrollX(); + final int width = getWidth(); + + final float marginOffset = (float) mPageMargin / width; + int itemIndex = 0; + ItemInfo ii = mItems.get(0); + float offset = ii.offset; + final int itemCount = mItems.size(); + final int firstPos = ii.position; + final int lastPos = mItems.get(itemCount - 1).position; + for (int pos = firstPos; pos < lastPos; pos++) { + while (pos > ii.position && itemIndex < itemCount) { + ii = mItems.get(++itemIndex); + } + + float drawAt; + if (pos == ii.position) { + drawAt = (ii.offset + ii.widthFactor) * width; + offset = ii.offset + ii.widthFactor + marginOffset; + } else { + float widthFactor = mAdapter.getPageWidth(pos); + drawAt = (offset + widthFactor) * width; + offset += widthFactor + marginOffset; + } + + if (drawAt + mPageMargin > scrollX) { + mMarginDrawable.setBounds(Math.round(drawAt), mTopPageBounds, + Math.round(drawAt + mPageMargin), mBottomPageBounds); + mMarginDrawable.draw(canvas); + } + + if (drawAt > scrollX + width) { + break; // No more visible, no sense in continuing + } + } + } + } + + /** + * Start a fake drag of the pager. + * + *

A fake drag can be useful if you want to synchronize the motion of the SliderPager + * with the touch scrolling of another view, while still letting the SliderPager + * control the snapping motion and fling behavior. (e.g. parallax-scrolling tabs.) + * Call {@link #fakeDragBy(float)} to simulate the actual drag motion. Call + * {@link #endFakeDrag()} to complete the fake drag and fling as necessary. + * + *

During a fake drag the SliderPager will ignore all touch events. If a real drag + * is already in progress, this method will return false. + * + * @return true if the fake drag began successfully, false if it could not be started. + * @see #fakeDragBy(float) + * @see #endFakeDrag() + */ + public boolean beginFakeDrag() { + if (mIsBeingDragged) { + return false; + } + mFakeDragging = true; + setScrollState(SCROLL_STATE_DRAGGING); + mInitialMotionX = mLastMotionX = 0; + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } else { + mVelocityTracker.clear(); + } + final long time = SystemClock.uptimeMillis(); + final MotionEvent ev = MotionEvent.obtain(time, time, MotionEvent.ACTION_DOWN, 0, 0, 0); + mVelocityTracker.addMovement(ev); + ev.recycle(); + mFakeDragBeginTime = time; + return true; + } + + /** + * End a fake drag of the pager. + * + * @see #beginFakeDrag() + * @see #fakeDragBy(float) + */ + public void endFakeDrag() { + if (!mFakeDragging) { + throw new IllegalStateException("No fake drag in progress. Call beginFakeDrag first."); + } + + if (mAdapter != null) { + final VelocityTracker velocityTracker = mVelocityTracker; + velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); + int initialVelocity = (int) velocityTracker.getXVelocity(mActivePointerId); + mPopulatePending = true; + final int width = getClientWidth(); + final int scrollX = getScrollX(); + final ItemInfo ii = infoForCurrentScrollPosition(); + final int currentPage = ii.position; + final float pageOffset = (((float) scrollX / width) - ii.offset) / ii.widthFactor; + final int totalDelta = (int) (mLastMotionX - mInitialMotionX); + int nextPage = determineTargetPage(currentPage, pageOffset, initialVelocity, + totalDelta); + setCurrentItemInternal(nextPage, true, true, initialVelocity); + } + endDrag(); + + mFakeDragging = false; + } + + /** + * Fake drag by an offset in pixels. You must have called {@link #beginFakeDrag()} first. + * + * @param xOffset Offset in pixels to drag by. + * @see #beginFakeDrag() + * @see #endFakeDrag() + */ + public void fakeDragBy(float xOffset) { + if (!mFakeDragging) { + throw new IllegalStateException("No fake drag in progress. Call beginFakeDrag first."); + } + + if (mAdapter == null) { + return; + } + + mLastMotionX += xOffset; + + float oldScrollX = getScrollX(); + float scrollX = oldScrollX - xOffset; + final int width = getClientWidth(); + + float leftBound = width * mFirstOffset; + float rightBound = width * mLastOffset; + + final ItemInfo firstItem = mItems.get(0); + final ItemInfo lastItem = mItems.get(mItems.size() - 1); + if (firstItem.position != 0) { + leftBound = firstItem.offset * width; + } + if (lastItem.position != mAdapter.getCount() - 1) { + rightBound = lastItem.offset * width; + } + + if (scrollX < leftBound) { + scrollX = leftBound; + } else if (scrollX > rightBound) { + scrollX = rightBound; + } + // Don't lose the rounded component + mLastMotionX += scrollX - (int) scrollX; + scrollTo((int) scrollX, getScrollY()); + pageScrolled((int) scrollX); + + // Synthesize an event for the VelocityTracker. + final long time = SystemClock.uptimeMillis(); + final MotionEvent ev = MotionEvent.obtain(mFakeDragBeginTime, time, MotionEvent.ACTION_MOVE, + mLastMotionX, 0, 0); + mVelocityTracker.addMovement(ev); + ev.recycle(); + } + + /** + * Returns true if a fake drag is in progress. + * + * @return true if currently in a fake drag, false otherwise. + * @see #beginFakeDrag() + * @see #fakeDragBy(float) + * @see #endFakeDrag() + */ + public boolean isFakeDragging() { + return mFakeDragging; + } + + private void onSecondaryPointerUp(MotionEvent ev) { + final int pointerIndex = ev.getActionIndex(); + final int pointerId = ev.getPointerId(pointerIndex); + if (pointerId == mActivePointerId) { + // This was our active pointer going up. Choose a new + // active pointer and adjust accordingly. + final int newPointerIndex = pointerIndex == 0 ? 1 : 0; + mLastMotionX = ev.getX(newPointerIndex); + mActivePointerId = ev.getPointerId(newPointerIndex); + if (mVelocityTracker != null) { + mVelocityTracker.clear(); + } + } + } + + private void endDrag() { + mIsBeingDragged = false; + mIsUnableToDrag = false; + + if (mVelocityTracker != null) { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + } + + private void setScrollingCacheEnabled(boolean enabled) { + if (mScrollingCacheEnabled != enabled) { + mScrollingCacheEnabled = enabled; + if (USE_CACHE) { + final int size = getChildCount(); + for (int i = 0; i < size; ++i) { + final View child = getChildAt(i); + if (child.getVisibility() != GONE) { + child.setDrawingCacheEnabled(enabled); + } + } + } + } + } + + /** + * Check if this SliderPager can be scrolled horizontally in a certain direction. + * + * @param direction Negative to check scrolling left, positive to check scrolling right. + * @return Whether this SliderPager can be scrolled in the specified direction. It will always + * return false if the specified direction is 0. + */ + @Override + public boolean canScrollHorizontally(int direction) { + if (mAdapter == null) { + return false; + } + + final int width = getClientWidth(); + final int scrollX = getScrollX(); + if (direction < 0) { + return (scrollX > (int) (width * mFirstOffset)); + } else if (direction > 0) { + return (scrollX < (int) (width * mLastOffset)); + } else { + return false; + } + } + + /** + * Tests scrollability within child views of v given a delta of dx. + * + * @param v View to test for horizontal scrollability + * @param checkV Whether the view v passed should itself be checked for scrollability (true), + * or just its children (false). + * @param dx Delta scrolled in pixels + * @param x X coordinate of the active touch point + * @param y Y coordinate of the active touch point + * @return true if child views of v can be scrolled by delta of dx. + */ + protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) { + if (v instanceof ViewGroup) { + final ViewGroup group = (ViewGroup) v; + final int scrollX = v.getScrollX(); + final int scrollY = v.getScrollY(); + final int count = group.getChildCount(); + // Count backwards - let topmost views consume scroll distance first. + for (int i = count - 1; i >= 0; i--) { + // This will not work for transformed views in Honeycomb+ + final View child = group.getChildAt(i); + if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight() + && y + scrollY >= child.getTop() && y + scrollY < child.getBottom() + && canScroll(child, true, dx, x + scrollX - child.getLeft(), + y + scrollY - child.getTop())) { + return true; + } + } + } + + return checkV && v.canScrollHorizontally(-dx); + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + // Let the focused view and/or our descendants get the key first + return super.dispatchKeyEvent(event) || executeKeyEvent(event); + } + + /** + * You can call this function yourself to have the scroll view perform + * scrolling from a key event, just as if the event had been dispatched to + * it by the view hierarchy. + * + * @param event The key event to execute. + * @return Return true if the event was handled, else false. + */ + public boolean executeKeyEvent(@NonNull KeyEvent event) { + boolean handled = false; + if (event.getAction() == KeyEvent.ACTION_DOWN) { + switch (event.getKeyCode()) { + case KeyEvent.KEYCODE_DPAD_LEFT: + if (event.hasModifiers(KeyEvent.META_ALT_ON)) { + handled = pageLeft(); + } else { + handled = arrowScroll(FOCUS_LEFT); + } + break; + case KeyEvent.KEYCODE_DPAD_RIGHT: + if (event.hasModifiers(KeyEvent.META_ALT_ON)) { + handled = pageRight(); + } else { + handled = arrowScroll(FOCUS_RIGHT); + } + break; + case KeyEvent.KEYCODE_TAB: + if (event.hasNoModifiers()) { + handled = arrowScroll(FOCUS_FORWARD); + } else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) { + handled = arrowScroll(FOCUS_BACKWARD); + } + break; + } + } + return handled; + } + + /** + * Handle scrolling in response to a left or right arrow click. + * + * @param direction The direction corresponding to the arrow key that was pressed. It should be + * either {@link #View#FOCUS_LEFT} or {@link #View#FOCUS_RIGHT}. + * @return Whether the scrolling was handled successfully. + */ + public boolean arrowScroll(int direction) { + View currentFocused = findFocus(); + if (currentFocused == this) { + currentFocused = null; + } else if (currentFocused != null) { + boolean isChild = false; + for (ViewParent parent = currentFocused.getParent(); parent instanceof ViewGroup; + parent = parent.getParent()) { + if (parent == this) { + isChild = true; + break; + } + } + if (!isChild) { + // This would cause the focus search down below to fail in fun ways. + final StringBuilder sb = new StringBuilder(); + sb.append(currentFocused.getClass().getSimpleName()); + for (ViewParent parent = currentFocused.getParent(); parent instanceof ViewGroup; + parent = parent.getParent()) { + sb.append(" => ").append(parent.getClass().getSimpleName()); + } + currentFocused = null; + } + } + + boolean handled = false; + + View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, + direction); + if (nextFocused != null && nextFocused != currentFocused) { + if (direction == View.FOCUS_LEFT) { + // If there is nothing to the left, or this is causing us to + // jump to the right, then what we really want to do is page left. + final int nextLeft = getChildRectInPagerCoordinates(mTempRect, nextFocused).left; + final int currLeft = getChildRectInPagerCoordinates(mTempRect, currentFocused).left; + if (currentFocused != null && nextLeft >= currLeft) { + handled = pageLeft(); + } else { + handled = nextFocused.requestFocus(); + } + } else if (direction == View.FOCUS_RIGHT) { + // If there is nothing to the right, or this is causing us to + // jump to the left, then what we really want to do is page right. + final int nextLeft = getChildRectInPagerCoordinates(mTempRect, nextFocused).left; + final int currLeft = getChildRectInPagerCoordinates(mTempRect, currentFocused).left; + if (currentFocused != null && nextLeft <= currLeft) { + handled = pageRight(); + } else { + handled = nextFocused.requestFocus(); + } + } + } else if (direction == FOCUS_LEFT || direction == FOCUS_BACKWARD) { + // Trying to move left and nothing there; try to page. + handled = pageLeft(); + } else if (direction == FOCUS_RIGHT || direction == FOCUS_FORWARD) { + // Trying to move right and nothing there; try to page. + handled = pageRight(); + } + if (handled) { + playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction)); + } + return handled; + } + + private Rect getChildRectInPagerCoordinates(Rect outRect, View child) { + if (outRect == null) { + outRect = new Rect(); + } + if (child == null) { + outRect.set(0, 0, 0, 0); + return outRect; + } + outRect.left = child.getLeft(); + outRect.right = child.getRight(); + outRect.top = child.getTop(); + outRect.bottom = child.getBottom(); + + ViewParent parent = child.getParent(); + while (parent instanceof ViewGroup && parent != this) { + final ViewGroup group = (ViewGroup) parent; + outRect.left += group.getLeft(); + outRect.right += group.getRight(); + outRect.top += group.getTop(); + outRect.bottom += group.getBottom(); + + parent = group.getParent(); + } + return outRect; + } + + boolean pageLeft() { + if (mCurItem > 0) { + setCurrentItem(mCurItem - 1, true); + return true; + } + return false; + } + + boolean pageRight() { + if (mAdapter != null && mCurItem < (mAdapter.getCount() - 1)) { + setCurrentItem(mCurItem + 1, true); + return true; + } + return false; + } + + /** + * We only want the current page that is being shown to be focusable. + */ + @Override + public void addFocusables(ArrayList views, int direction, int focusableMode) { + final int focusableCount = views.size(); + + final int descendantFocusability = getDescendantFocusability(); + + if (descendantFocusability != FOCUS_BLOCK_DESCENDANTS) { + for (int i = 0; i < getChildCount(); i++) { + final View child = getChildAt(i); + if (child.getVisibility() == VISIBLE) { + ItemInfo ii = infoForChild(child); + if (ii != null && ii.position == mCurItem) { + child.addFocusables(views, direction, focusableMode); + } + } + } + } + + // we add ourselves (if focusable) in all cases except for when we are + // FOCUS_AFTER_DESCENDANTS and there are some descendants focusable. this is + // to avoid the focus search finding layouts when a more precise search + // among the focusable children would be more interesting. + if (descendantFocusability != FOCUS_AFTER_DESCENDANTS + || (focusableCount == views.size())) { // No focusable descendants + // Note that we can't call the superclass here, because it will + // add all views in. So we need to do the same thing View does. + if (!isFocusable()) { + return; + } + if ((focusableMode & FOCUSABLES_TOUCH_MODE) == FOCUSABLES_TOUCH_MODE + && isInTouchMode() && !isFocusableInTouchMode()) { + return; + } + if (views != null) { + views.add(this); + } + } + } + + /** + * We only want the current page that is being shown to be touchable. + */ + @Override + public void addTouchables(ArrayList views) { + // Note that we don't call super.addTouchables(), which means that + // we don't call View.addTouchables(). This is okay because a SliderPager + // is itself not touchable. + for (int i = 0; i < getChildCount(); i++) { + final View child = getChildAt(i); + if (child.getVisibility() == VISIBLE) { + ItemInfo ii = infoForChild(child); + if (ii != null && ii.position == mCurItem) { + child.addTouchables(views); + } + } + } + } + + /** + * We only want the current page that is being shown to be focusable. + */ + @Override + protected boolean onRequestFocusInDescendants(int direction, + Rect previouslyFocusedRect) { + int index; + int increment; + int end; + int count = getChildCount(); + if ((direction & FOCUS_FORWARD) != 0) { + index = 0; + increment = 1; + end = count; + } else { + index = count - 1; + increment = -1; + end = -1; + } + for (int i = index; i != end; i += increment) { + View child = getChildAt(i); + if (child.getVisibility() == VISIBLE) { + ItemInfo ii = infoForChild(child); + if (ii != null && ii.position == mCurItem) { + if (child.requestFocus(direction, previouslyFocusedRect)) { + return true; + } + } + } + } + return false; + } + + @SuppressLint("WrongConstant") + @Override + public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { + // Dispatch scroll events from this SliderPager. + if (event.getEventType() == AccessibilityEventCompat.TYPE_VIEW_SCROLLED) { + return super.dispatchPopulateAccessibilityEvent(event); + } + + // Dispatch all other accessibility events from the current page. + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + if (child.getVisibility() == VISIBLE) { + final ItemInfo ii = infoForChild(child); + if (ii != null && ii.position == mCurItem + && child.dispatchPopulateAccessibilityEvent(event)) { + return true; + } + } + } + + return false; + } + + @Override + protected ViewGroup.LayoutParams generateDefaultLayoutParams() { + return new LayoutParams(); + } + + @Override + protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { + return generateDefaultLayoutParams(); + } + + @Override + protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { + return p instanceof LayoutParams && super.checkLayoutParams(p); + } + + @Override + public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { + return new LayoutParams(getContext(), attrs); + } + + /** + * Callback interface for responding to changing state of the selected page. + */ + public interface OnPageChangeListener { + + /** + * This method will be invoked when the current page is scrolled, either as part + * of a programmatically initiated smooth scroll or a user initiated touch scroll. + * + * @param position Position index of the first page currently being displayed. + * Page position+1 will be visible if positionOffset is nonzero. + * @param positionOffset Value from [0, 1) indicating the offset from the page at position. + * @param positionOffsetPixels Value in pixels indicating the offset from position. + */ + void onPageScrolled(int position, float positionOffset, @Px int positionOffsetPixels); + + /** + * This method will be invoked when a new page becomes selected. Animation is not + * necessarily complete. + * + * @param position Position index of the new selected page. + */ + void onPageSelected(int position); + + /** + * Called when the scroll state changes. Useful for discovering when the user + * begins dragging, when the pager is automatically settling to the current page, + * or when it is fully stopped/idle. + * + * @param state The new scroll state. + * @see SliderPager#SCROLL_STATE_IDLE + * @see SliderPager#SCROLL_STATE_DRAGGING + * @see SliderPager#SCROLL_STATE_SETTLING + */ + void onPageScrollStateChanged(int state); + } + + /** + * A PageTransformer is invoked whenever a visible/attached page is scrolled. + * This offers an opportunity for the application to apply a custom transformation + * to the page views using animation properties. + * + *

As property animation is only supported as of Android 3.0 and forward, + * setting a PageTransformer on a SliderPager on earlier platform versions will + * be ignored.

+ */ + public interface PageTransformer { + /** + * Apply a property transformation to the given page. + * + * @param page Apply the transformation to this page + * @param position Position of page relative to the current front-and-center + * position of the pager. 0 is front and center. 1 is one full + * page position to the right, and -1 is one page position to the left. + */ + void transformPage(@NonNull View page, float position); + } + + /** + * Callback interface for responding to adapter changes. + */ + public interface OnAdapterChangeListener { + /** + * Called when the adapter for the given view pager has changed. + * + * @param viewPager SliderPager where the adapter change has happened + * @param oldAdapter the previously set adapter + * @param newAdapter the newly set adapter + */ + void onAdapterChanged(@NonNull SliderPager viewPager, + @Nullable PagerAdapter oldAdapter, @Nullable PagerAdapter newAdapter); + } + + /** + * Annotation which allows marking of views to be decoration views when added to a view + * pager. + * + *

Views marked with this annotation can be added to the view pager with a layout resource. + * An example being {@link ##PagerTitleStrip}.

+ * + *

You can also control whether a view is a decor view but setting + * {@link ##LayoutParams#isDecor} on the child's layout params.

+ */ + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + @Inherited + public @interface DecorView { + } + + static class ItemInfo { + Object object; + int position; + boolean scrolling; + float widthFactor; + float offset; + } + + /** + * Simple implementation of the {@link ##OnPageChangeListener} interface with stub + * implementations of each method. Extend this if you do not intend to override + * every method of {@link ##OnPageChangeListener}. + */ + public static class SimpleOnPageChangeListener implements OnPageChangeListener { + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + // This space for rent + } + + @Override + public void onPageSelected(int position) { + // This space for rent + } + + @Override + public void onPageScrollStateChanged(int state) { + // This space for rent + } + } + + /** + * This is the persistent state that is saved by SliderPager. Only needed + * if you are creating a sublass of SliderPager that must save its own + * state, in which case it should implement a subclass of this which + * contains that state. + */ + public static class SavedState extends AbsSavedState { + public static final Parcelable.Creator CREATOR = new Parcelable.ClassLoaderCreator() { + @Override + public SavedState createFromParcel(Parcel in, ClassLoader loader) { + return new SavedState(in, loader); + } + + @Override + public SavedState createFromParcel(Parcel in) { + return new SavedState(in, null); + } + + @Override + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + int position; + Parcelable adapterState; + ClassLoader loader; + + public SavedState(@NonNull Parcelable superState) { + super(superState); + } + + SavedState(Parcel in, ClassLoader loader) { + super(in, loader); + if (loader == null) { + loader = getClass().getClassLoader(); + } + position = in.readInt(); + adapterState = in.readParcelable(loader); + this.loader = loader; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + super.writeToParcel(out, flags); + out.writeInt(position); + out.writeParcelable(adapterState, flags); + } + + @Override + public String toString() { + return "FragmentPager.SavedState{" + + Integer.toHexString(System.identityHashCode(this)) + + " position=" + position + "}"; + } + } + + /** + * Layout parameters that should be supplied for views added to a + * SliderPager. + */ + public static class LayoutParams extends ViewGroup.LayoutParams { + /** + * true if this view is a decoration on the pager itself and not + * a view supplied by the adapter. + */ + public boolean isDecor; + + /** + * Gravity setting for use on decor views only: + * Where to position the view page within the overall SliderPager + * container; constants are defined in {@link ##Gravity}. + */ + public int gravity; + + /** + * Width as a 0-1 multiplier of the measured pager width + */ + float widthFactor = 0.f; + + /** + * true if this view was added during layout and needs to be measured + * before being positioned. + */ + boolean needsMeasure; + + /** + * Adapter position this view is for if !isDecor + */ + int position; + + /** + * Current child index within the SliderPager that this view occupies + */ + int childIndex; + + public LayoutParams() { + super(MATCH_PARENT, MATCH_PARENT); + } + + public LayoutParams(Context context, AttributeSet attrs) { + super(context, attrs); + + final TypedArray a = context.obtainStyledAttributes(attrs, LAYOUT_ATTRS); + gravity = a.getInteger(0, Gravity.TOP); + a.recycle(); + } + } + + static class ViewPositionComparator implements Comparator { + @Override + public int compare(View lhs, View rhs) { + final LayoutParams llp = (LayoutParams) lhs.getLayoutParams(); + final LayoutParams rlp = (LayoutParams) rhs.getLayoutParams(); + if (llp.isDecor != rlp.isDecor) { + return llp.isDecor ? 1 : -1; + } + return llp.position - rlp.position; + } + } + + class MyAccessibilityDelegate extends AccessibilityDelegateCompat { + + @Override + public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(host, event); + event.setClassName(SliderPager.class.getName()); + event.setScrollable(canScroll()); + if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SCROLLED && mAdapter != null) { + event.setItemCount(mAdapter.getCount()); + event.setFromIndex(mCurItem); + event.setToIndex(mCurItem); + } + } + + @Override + public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) { + super.onInitializeAccessibilityNodeInfo(host, info); + info.setClassName(SliderPager.class.getName()); + info.setScrollable(canScroll()); + if (canScrollHorizontally(1)) { + info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD); + } + if (canScrollHorizontally(-1)) { + info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD); + } + } + + @Override + public boolean performAccessibilityAction(View host, int action, Bundle args) { + if (super.performAccessibilityAction(host, action, args)) { + return true; + } + switch (action) { + case AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD: { + if (canScrollHorizontally(1)) { + setCurrentItem(mCurItem + 1); + return true; + } + } + return false; + case AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD: { + if (canScrollHorizontally(-1)) { + setCurrentItem(mCurItem - 1); + return true; + } + } + return false; + } + return false; + } + + private boolean canScroll() { + return (mAdapter != null) && (mAdapter.getCount() > 1); + } + } + + private class PagerObserver extends DataSetObserver { + PagerObserver() { + } + + @Override + public void onChanged() { + dataSetChanged(); + } + + @Override + public void onInvalidated() { + dataSetChanged(); + } + } + + class OwnScroller extends Scroller { + + private final int durationScrollMillis; + + OwnScroller(Context context, int durationScroll) { + super(context, sInterpolator); + this.durationScrollMillis = durationScroll; + } + + OwnScroller(Context context, int durationScroll, Interpolator interpolator) { + super(context, interpolator); + this.durationScrollMillis = durationScroll; + } + + @Override + public void startScroll(int startX, int startY, int dx, int dy, int duration) { + super.startScroll(startX, startY, dx, dy, durationScrollMillis); + } + } +} diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/SliderView.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/SliderView.java new file mode 100644 index 000000000..609f5a2c6 --- /dev/null +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/SliderView.java @@ -0,0 +1,740 @@ +package com.smarteist.autoimageslider; + +import static com.smarteist.autoimageslider.IndicatorView.draw.controller.AttributeController.getRtlMode; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Color; +import android.os.Handler; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.MotionEvent; +import android.view.View; +import android.view.animation.Interpolator; +import android.widget.FrameLayout; + +import androidx.annotation.NonNull; +import androidx.core.view.ViewCompat; +import androidx.viewpager.widget.PagerAdapter; + +import com.smarteist.autoimageslider.IndicatorView.PageIndicatorView; +import com.smarteist.autoimageslider.IndicatorView.animation.type.BaseAnimation; +import com.smarteist.autoimageslider.IndicatorView.animation.type.ColorAnimation; +import com.smarteist.autoimageslider.IndicatorView.animation.type.IndicatorAnimationType; +import com.smarteist.autoimageslider.IndicatorView.draw.controller.DrawController; +import com.smarteist.autoimageslider.IndicatorView.draw.data.Orientation; +import com.smarteist.autoimageslider.IndicatorView.draw.data.RtlMode; +import com.smarteist.autoimageslider.IndicatorView.utils.DensityUtils; +import com.smarteist.autoimageslider.InfiniteAdapter.InfinitePagerAdapter; +import com.smarteist.autoimageslider.Transformations.AntiClockSpinTransformation; +import com.smarteist.autoimageslider.Transformations.Clock_SpinTransformation; +import com.smarteist.autoimageslider.Transformations.CubeInDepthTransformation; +import com.smarteist.autoimageslider.Transformations.CubeInRotationTransformation; +import com.smarteist.autoimageslider.Transformations.CubeInScalingTransformation; +import com.smarteist.autoimageslider.Transformations.CubeOutDepthTransformation; +import com.smarteist.autoimageslider.Transformations.CubeOutRotationTransformation; +import com.smarteist.autoimageslider.Transformations.CubeOutScalingTransformation; +import com.smarteist.autoimageslider.Transformations.DepthTransformation; +import com.smarteist.autoimageslider.Transformations.FadeTransformation; +import com.smarteist.autoimageslider.Transformations.FanTransformation; +import com.smarteist.autoimageslider.Transformations.FidgetSpinTransformation; +import com.smarteist.autoimageslider.Transformations.GateTransformation; +import com.smarteist.autoimageslider.Transformations.HingeTransformation; +import com.smarteist.autoimageslider.Transformations.HorizontalFlipTransformation; +import com.smarteist.autoimageslider.Transformations.PopTransformation; +import com.smarteist.autoimageslider.Transformations.SimpleTransformation; +import com.smarteist.autoimageslider.Transformations.SpinnerTransformation; +import com.smarteist.autoimageslider.Transformations.TossTransformation; +import com.smarteist.autoimageslider.Transformations.VerticalFlipTransformation; +import com.smarteist.autoimageslider.Transformations.VerticalShutTransformation; +import com.smarteist.autoimageslider.Transformations.ZoomOutTransformation; + +public class SliderView extends FrameLayout + implements Runnable, View.OnTouchListener, + SliderViewAdapter.DataSetListener, SliderPager.OnPageChangeListener { + + public static final int AUTO_CYCLE_DIRECTION_RIGHT = 0; + public static final int AUTO_CYCLE_DIRECTION_LEFT = 1; + public static final int AUTO_CYCLE_DIRECTION_BACK_AND_FORTH = 2; + public static final String TAG = "Slider View : "; + + private final Handler mHandler = new Handler(); + private boolean mFlagBackAndForth; + private boolean mIsAutoCycle; + private int mAutoCycleDirection; + private int mScrollTimeInMillis; + private PageIndicatorView mPagerIndicator; + private SliderViewAdapter mPagerAdapter; + private SliderPager mSliderPager; + private InfinitePagerAdapter mInfinitePagerAdapter; + private OnSliderPageListener mPageListener; + private boolean mIsInfiniteAdapter = true; + private boolean mIsIndicatorEnabled = true; + private int mPreviousPosition = -1; + + /*Constructor*/ + public SliderView(Context context) { + super(context); + setupSlideView(context); + } + + public SliderView(Context context, AttributeSet attrs) { + super(context, attrs); + setupSlideView(context); + setUpAttributes(context, attrs); + } + + public SliderView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + setupSlideView(context); + setUpAttributes(context, attrs); + } + /*Constructor*/ + + /** + * This class syncs all attributes from xml tag for this slider. + * + * @param context its android main context which is needed. + * @param attrs attributes from xml slider tags. + */ + private void setUpAttributes(@NonNull Context context, AttributeSet attrs) { + TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.SliderView, 0, 0); + + boolean indicatorEnabled = typedArray.getBoolean(R.styleable.SliderView_sliderIndicatorEnabled, true); + int sliderAnimationDuration = typedArray.getInt(R.styleable.SliderView_sliderAnimationDuration, SliderPager.DEFAULT_SCROLL_DURATION); + int sliderScrollTimeInSec = typedArray.getInt(R.styleable.SliderView_sliderScrollTimeInSec, 2); + boolean sliderAutoCycleEnabled = typedArray.getBoolean(R.styleable.SliderView_sliderAutoCycleEnabled, true); + boolean sliderStartAutoCycle = typedArray.getBoolean(R.styleable.SliderView_sliderStartAutoCycle, false); + int sliderAutoCycleDirection = typedArray.getInt(R.styleable.SliderView_sliderAutoCycleDirection, AUTO_CYCLE_DIRECTION_RIGHT); + + setSliderAnimationDuration(sliderAnimationDuration); + setScrollTimeInSec(sliderScrollTimeInSec); + setAutoCycle(sliderAutoCycleEnabled); + setAutoCycleDirection(sliderAutoCycleDirection); + setAutoCycle(sliderStartAutoCycle); + setIndicatorEnabled(indicatorEnabled); + + /*start indicator configs*/ + if (mIsIndicatorEnabled) { + initIndicator(); + int indicatorOrientation = typedArray.getInt(R.styleable.SliderView_sliderIndicatorOrientation, Orientation.HORIZONTAL.ordinal()); + Orientation orientation; + if (indicatorOrientation == 0) { + orientation = Orientation.HORIZONTAL; + } else { + orientation = Orientation.VERTICAL; + } + int indicatorRadius = (int) typedArray.getDimension(R.styleable.SliderView_sliderIndicatorRadius, DensityUtils.dpToPx(2)); + int indicatorPadding = (int) typedArray.getDimension(R.styleable.SliderView_sliderIndicatorPadding, DensityUtils.dpToPx(3)); + int indicatorMargin = (int) typedArray.getDimension(R.styleable.SliderView_sliderIndicatorMargin, DensityUtils.dpToPx(12)); + int indicatorMarginLeft = (int) typedArray.getDimension(R.styleable.SliderView_sliderIndicatorMarginLeft, DensityUtils.dpToPx(12)); + int indicatorMarginTop = (int) typedArray.getDimension(R.styleable.SliderView_sliderIndicatorMarginTop, DensityUtils.dpToPx(12)); + int indicatorMarginRight = (int) typedArray.getDimension(R.styleable.SliderView_sliderIndicatorMarginRight, DensityUtils.dpToPx(12)); + int indicatorMarginBottom = (int) typedArray.getDimension(R.styleable.SliderView_sliderIndicatorMarginBottom, DensityUtils.dpToPx(12)); + int indicatorGravity = typedArray.getInt(R.styleable.SliderView_sliderIndicatorGravity, Gravity.CENTER | Gravity.BOTTOM); + int indicatorUnselectedColor = typedArray.getColor(R.styleable.SliderView_sliderIndicatorUnselectedColor, Color.parseColor(ColorAnimation.DEFAULT_UNSELECTED_COLOR)); + int indicatorSelectedColor = typedArray.getColor(R.styleable.SliderView_sliderIndicatorSelectedColor, Color.parseColor(ColorAnimation.DEFAULT_SELECTED_COLOR)); + int indicatorAnimationDuration = typedArray.getInt(R.styleable.SliderView_sliderIndicatorAnimationDuration, BaseAnimation.DEFAULT_ANIMATION_TIME); + int indicatorRtlMode = typedArray.getInt(R.styleable.SliderView_sliderIndicatorRtlMode, RtlMode.Off.ordinal()); + RtlMode rtlMode = getRtlMode(indicatorRtlMode); + + setIndicatorOrientation(orientation); + setIndicatorRadius(indicatorRadius); + setIndicatorPadding(indicatorPadding); + setIndicatorMargin(indicatorMargin); + setIndicatorMarginCustom(indicatorMarginLeft, indicatorMarginTop, indicatorMarginRight, indicatorMarginBottom); + setIndicatorGravity(indicatorGravity); + setIndicatorMargins(indicatorMarginLeft, indicatorMarginTop, indicatorMarginRight, indicatorMarginBottom); + setIndicatorUnselectedColor(indicatorUnselectedColor); + setIndicatorSelectedColor(indicatorSelectedColor); + setIndicatorAnimationDuration(indicatorAnimationDuration); + setIndicatorRtlMode(rtlMode); + } + /*end indicator configs*/ + + typedArray.recycle(); + } + + /** + * This method will be called only if {@link #mIsIndicatorEnabled} is true. + * so initializes indicator if its active. + */ + private void initIndicator() { + if (mPagerIndicator == null) { + mPagerIndicator = new PageIndicatorView(getContext()); + LayoutParams params = new LayoutParams( + LayoutParams.WRAP_CONTENT, + LayoutParams.WRAP_CONTENT + ); + params.gravity = Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM; + params.setMargins(20, 20, 20, 20); + addView(mPagerIndicator, 1, params); + } + mPagerIndicator.setViewPager(mSliderPager); + mPagerIndicator.setDynamicCount(true); + } + + /** + * This method fires initialization jobs for + * slider view. + * + * @param context its android main context which is needed. + */ + @SuppressLint("ClickableViewAccessibility") + private void setupSlideView(Context context) { + mSliderPager = new SliderPager(context); + mSliderPager.setOverScrollMode(OVER_SCROLL_IF_CONTENT_SCROLLS); + mSliderPager.setId(ViewCompat.generateViewId()); + LayoutParams sliderParams = new LayoutParams( + LayoutParams.MATCH_PARENT, + LayoutParams.MATCH_PARENT + ); + addView(mSliderPager, 0, sliderParams); + mSliderPager.setOnTouchListener(this); + mSliderPager.addOnPageChangeListener(this); + } + + /** + * @param listener for indicator dots clicked. + */ + public void setOnIndicatorClickListener(DrawController.ClickListener listener) { + mPagerIndicator.setClickListener(listener); + } + + /** + * @param listener is a callback of current item in sliderView. + */ + public void setCurrentPageListener(OnSliderPageListener listener) { + this.mPageListener = listener; + } + + /** + * @param pagerAdapter Set a SliderAdapter that will supply views + * for this slider as needed. + */ + public void setSliderAdapter(@NonNull SliderViewAdapter pagerAdapter, boolean infiniteAdapter) { + this.mIsInfiniteAdapter = infiniteAdapter; + if (!infiniteAdapter) { + this.mPagerAdapter = pagerAdapter; + this.mSliderPager.setAdapter(pagerAdapter); + } else { + setSliderAdapter(pagerAdapter); + } + } + + public void setInfiniteAdapterEnabled(boolean enabled) { + if (mPagerAdapter != null) { + setSliderAdapter(mPagerAdapter, enabled); + } + } + + /** + * @return Sliders Pager + */ + public SliderPager getSliderPager() { + return mSliderPager; + } + + /** + * @return adapter of current slider. + */ + public PagerAdapter getSliderAdapter() { + return mPagerAdapter; + } + + /** + * @param pagerAdapter Set a SliderAdapter that will supply views + * for this slider as needed. + */ + public void setSliderAdapter(@NonNull SliderViewAdapter pagerAdapter) { + mPagerAdapter = pagerAdapter; + //set slider adapter + mInfinitePagerAdapter = new InfinitePagerAdapter(pagerAdapter); + //registerAdapterDataObserver(); + mSliderPager.setAdapter(mInfinitePagerAdapter); + mPagerAdapter.dataSetChangedListener(this); + // set slider on correct position whether its infinite or not. + setCurrentPagePosition(0); + } + + /** + * @return if is slider auto cycling or not? + */ + public boolean isAutoCycle() { + return mIsAutoCycle; + } + + public void setAutoCycle(boolean autoCycle) { + this.mIsAutoCycle = autoCycle; + } + + /** + * @param limit How many pages will be kept offscreen in an idle state. + *

You should keep this limit low, especially if your pages have complex layouts. + * * This setting defaults to 1.

+ */ + public void setOffscreenPageLimit(int limit) { + mSliderPager.setOffscreenPageLimit(limit); + } + + /** + * @return sliding delay in seconds. + */ + public int getScrollTimeInSec() { + return mScrollTimeInMillis / 1000; + } + + /** + * @param time of sliding delay in seconds. + */ + public void setScrollTimeInSec(int time) { + mScrollTimeInMillis = time * 1000; + } + + public int getScrollTimeInMillis() { + return mScrollTimeInMillis; + } + + public void setScrollTimeInMillis(int millis) { + this.mScrollTimeInMillis = millis; + } + + /** + * @param animation changes pre defined animations for slider. + */ + public void setSliderTransformAnimation(SliderAnimations animation) { + + switch (animation) { + case ANTICLOCKSPINTRANSFORMATION: + mSliderPager.setPageTransformer(false, new AntiClockSpinTransformation()); + break; + case CLOCK_SPINTRANSFORMATION: + mSliderPager.setPageTransformer(false, new Clock_SpinTransformation()); + break; + case CUBEINDEPTHTRANSFORMATION: + mSliderPager.setPageTransformer(false, new CubeInDepthTransformation()); + break; + case CUBEINROTATIONTRANSFORMATION: + mSliderPager.setPageTransformer(false, new CubeInRotationTransformation()); + break; + case CUBEINSCALINGTRANSFORMATION: + mSliderPager.setPageTransformer(false, new CubeInScalingTransformation()); + break; + case CUBEOUTDEPTHTRANSFORMATION: + mSliderPager.setPageTransformer(false, new CubeOutDepthTransformation()); + break; + case CUBEOUTROTATIONTRANSFORMATION: + mSliderPager.setPageTransformer(false, new CubeOutRotationTransformation()); + break; + case CUBEOUTSCALINGTRANSFORMATION: + mSliderPager.setPageTransformer(false, new CubeOutScalingTransformation()); + break; + case DEPTHTRANSFORMATION: + mSliderPager.setPageTransformer(false, new DepthTransformation()); + break; + case FADETRANSFORMATION: + mSliderPager.setPageTransformer(false, new FadeTransformation()); + break; + case FANTRANSFORMATION: + mSliderPager.setPageTransformer(false, new FanTransformation()); + break; + case FIDGETSPINTRANSFORMATION: + mSliderPager.setPageTransformer(false, new FidgetSpinTransformation()); + break; + case GATETRANSFORMATION: + mSliderPager.setPageTransformer(false, new GateTransformation()); + break; + case HINGETRANSFORMATION: + mSliderPager.setPageTransformer(false, new HingeTransformation()); + break; + case HORIZONTALFLIPTRANSFORMATION: + mSliderPager.setPageTransformer(false, new HorizontalFlipTransformation()); + break; + case POPTRANSFORMATION: + mSliderPager.setPageTransformer(false, new PopTransformation()); + break; + case SPINNERTRANSFORMATION: + mSliderPager.setPageTransformer(false, new SpinnerTransformation()); + break; + case TOSSTRANSFORMATION: + mSliderPager.setPageTransformer(false, new TossTransformation()); + break; + case VERTICALFLIPTRANSFORMATION: + mSliderPager.setPageTransformer(false, new VerticalFlipTransformation()); + break; + case VERTICALSHUTTRANSFORMATION: + mSliderPager.setPageTransformer(false, new VerticalShutTransformation()); + break; + case ZOOMOUTTRANSFORMATION: + mSliderPager.setPageTransformer(false, new ZoomOutTransformation()); + break; + default: + mSliderPager.setPageTransformer(false, new SimpleTransformation()); + + } + + } + + @Override + public boolean onTouch(View v, MotionEvent event) { + if (isAutoCycle()) { + if (event.getAction() == MotionEvent.ACTION_MOVE) { + stopAutoCycle(); + } else if (event.getAction() == MotionEvent.ACTION_UP) { + // resume after ~2 seconds debounce. + mHandler.postDelayed(new Runnable() { + @Override + public void run() { + startAutoCycle(); + } + }, 2000); + } + } + return false; + } + + /** + * @param animation set slider animation manually . + * it accepts {@link ##PageTransformer} animation classes. + */ + public void setCustomSliderTransformAnimation(SliderPager.PageTransformer animation) { + mSliderPager.setPageTransformer(false, animation); + } + + /** + * @param duration changes slider animation duration. + */ + public void setSliderAnimationDuration(int duration) { + mSliderPager.setScrollDuration(duration); + } + + /** + * @param duration changes slider animation duration. + * @param interpolator its animation duration accelerator + * An interpolator defines the rate of change of an animation + */ + public void setSliderAnimationDuration(int duration, Interpolator interpolator) { + mSliderPager.setScrollDuration(duration, interpolator); + } + + /** + * @return Nullable position of current sliding item. + */ + public int getCurrentPagePosition() { + + if (getSliderAdapter() != null) { + return getSliderPager().getCurrentItem(); + } else { + throw new NullPointerException("Adapter not set"); + } + } + + /** + * This method handles correct position whether slider is on infinite mode or not + * + * @param position changes position of slider + * items manually. + */ + public void setCurrentPagePosition(int position) { + mSliderPager.setCurrentItem(position, true); + } + + public PageIndicatorView getPagerIndicator() { + return this.mPagerIndicator; + } + + public void setPageIndicatorView(PageIndicatorView indicatorView) { + this.mPagerIndicator = indicatorView; + initIndicator(); + } + + public void setIndicatorEnabled(boolean enabled) { + this.mIsIndicatorEnabled = enabled; + if (mPagerIndicator == null && enabled) { + initIndicator(); + } + } + + /** + * @param duration modifies indicator animation duration. + */ + public void setIndicatorAnimationDuration(long duration) { + mPagerIndicator.setAnimationDuration(duration); + } + + /** + * @param gravity {@link #View} integer gravity of indicator dots. + */ + public void setIndicatorGravity(int gravity) { + FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) mPagerIndicator.getLayoutParams(); + layoutParams.gravity = gravity; + mPagerIndicator.setLayoutParams(layoutParams); + } + + /** + * @param padding changes indicator padding. + */ + public void setIndicatorPadding(int padding) { + mPagerIndicator.setPadding(padding); + } + + /** + * Sets the indicator margins, in pixels. + * + * @param left the left margin size + * @param top the top margin size + * @param right the right margin size + * @param bottom the bottom margin size + */ + public void setIndicatorMargins(int left, int top, int right, int bottom) { + FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) mPagerIndicator.getLayoutParams(); + layoutParams.setMargins(left, top, right, bottom); + mPagerIndicator.setLayoutParams(layoutParams); + } + + /** + * @param orientation changes orientation of indicator dots. + */ + public void setIndicatorOrientation(Orientation orientation) { + mPagerIndicator.setOrientation(orientation); + } + + /** + * @param animation {@link #SliderView#IndicatorAnimationType} of indicator dots + */ + public void setIndicatorAnimation(IndicatorAnimationType animation) { + mPagerIndicator.setAnimationType(animation); + } + + /** + * @param visibility this method changes indicator visibility + */ + public void setIndicatorVisibility(boolean visibility) { + if (visibility) { + mPagerIndicator.setVisibility(VISIBLE); + } else { + mPagerIndicator.setVisibility(GONE); + } + } + + /** + * @return number of items in {@link #SliderView#SliderViewAdapter)} + */ + private int getAdapterItemsCount() { + try { + return getSliderAdapter().getCount(); + } catch (NullPointerException e) { + return 0; + } + } + + /** + * This method stars the auto cycling + */ + public void startAutoCycle() { + //clean previous callbacks + mHandler.removeCallbacks(this); + + //Run the loop for the first time + mHandler.postDelayed(this, mScrollTimeInMillis); + } + + /** + * This method cancels the auto cycling + */ + public void stopAutoCycle() { + //clean callback + mHandler.removeCallbacks(this); + } + + /** + * @return direction of auto cycling + * {@value AUTO_CYCLE_DIRECTION_LEFT} + * {@value AUTO_CYCLE_DIRECTION_RIGHT} + * {@value AUTO_CYCLE_DIRECTION_BACK_AND_FORTH} + */ + public int getAutoCycleDirection() { + return mAutoCycleDirection; + } + + /** + * This method setting direction of sliders auto cycling + * accepts constant values defined in {@link #SliderView} class + * {@value AUTO_CYCLE_DIRECTION_LEFT} + * {@value AUTO_CYCLE_DIRECTION_RIGHT} + * {@value AUTO_CYCLE_DIRECTION_BACK_AND_FORTH} + */ + public void setAutoCycleDirection(int direction) { + mAutoCycleDirection = direction; + } + + /** + * @return size of indicator dot + */ + public int getIndicatorRadius() { + return mPagerIndicator.getRadius(); + } + + /** + * @param pagerIndicatorRadius modifies size of indicator dots + */ + public void setIndicatorRadius(int pagerIndicatorRadius) { + this.mPagerIndicator.setRadius(pagerIndicatorRadius); + } + + /** + * @param rtlMode for indicator sliding direction + */ + public void setIndicatorRtlMode(RtlMode rtlMode) { + mPagerIndicator.setRtlMode(rtlMode); + } + + /** + * @param margin modifies indicator margin + */ + public void setIndicatorMargin(int margin) { + FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) mPagerIndicator.getLayoutParams(); + layoutParams.setMargins(margin, margin, margin, margin); + mPagerIndicator.setLayoutParams(layoutParams); + } + + public void setIndicatorMarginCustom(int left, int top, int right, int bottom) { + FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) mPagerIndicator.getLayoutParams(); + layoutParams.setMargins(left, top, right, bottom); + mPagerIndicator.setLayoutParams(layoutParams); + } + + /** + * @return color of selected dot + */ + public int getIndicatorSelectedColor() { + return this.mPagerIndicator.getSelectedColor(); + } + + /** + * @param color setting color of selected dot + */ + public void setIndicatorSelectedColor(int color) { + this.mPagerIndicator.setSelectedColor(color); + } + + /** + * @return color of unselected dots + */ + public int getIndicatorUnselectedColor() { + return this.mPagerIndicator.getUnselectedColor(); + } + + public void setIndicatorUnselectedColor(int color) { + this.mPagerIndicator.setUnselectedColor(color); + } + + /** + * This method handles sliding behaviors + * which passed into {@link #SliderView#mHandler} + *

+ * see {@link #SliderView#startAutoCycle()} + */ + @Override + public void run() { + try { + slideToNextPosition(); + } finally { + if (mIsAutoCycle) { + // continue the loop + mHandler.postDelayed(this, mScrollTimeInMillis); + } + } + } + + public void slideToNextPosition() { + + int currentPosition = mSliderPager.getCurrentItem(); + int adapterItemsCount = getAdapterItemsCount(); + if (adapterItemsCount > 1) { + if (mAutoCycleDirection == AUTO_CYCLE_DIRECTION_BACK_AND_FORTH) { + if (currentPosition % (adapterItemsCount - 1) == 0 && mPreviousPosition != getAdapterItemsCount() - 1 && mPreviousPosition != 0) { + mFlagBackAndForth = !mFlagBackAndForth; + } + if (mFlagBackAndForth) { + mSliderPager.setCurrentItem(currentPosition + 1, true); + } else { + mSliderPager.setCurrentItem(currentPosition - 1, true); + } + } + if (mAutoCycleDirection == AUTO_CYCLE_DIRECTION_LEFT) { + mSliderPager.setCurrentItem(currentPosition - 1, true); + } + if (mAutoCycleDirection == AUTO_CYCLE_DIRECTION_RIGHT) { + mSliderPager.setCurrentItem(currentPosition + 1, true); + } + } + mPreviousPosition = currentPosition; + } + + + public void slideToPreviousPosition() { + + int currentPosition = mSliderPager.getCurrentItem(); + int adapterItemsCount = getAdapterItemsCount(); + + if (adapterItemsCount > 1) { + if (mAutoCycleDirection == AUTO_CYCLE_DIRECTION_BACK_AND_FORTH) { + if (currentPosition % (adapterItemsCount - 1) == 0 && mPreviousPosition != getAdapterItemsCount() - 1 && mPreviousPosition != 0) { + mFlagBackAndForth = !mFlagBackAndForth; + } + if (mFlagBackAndForth && currentPosition < mPreviousPosition) { + mSliderPager.setCurrentItem(currentPosition - 1, true); + } else { + mSliderPager.setCurrentItem(currentPosition + 1, true); + } + } + if (mAutoCycleDirection == AUTO_CYCLE_DIRECTION_LEFT) { + mSliderPager.setCurrentItem(currentPosition + 1, true); + } + if (mAutoCycleDirection == AUTO_CYCLE_DIRECTION_RIGHT) { + mSliderPager.setCurrentItem(currentPosition - 1, true); + } + } + mPreviousPosition = currentPosition; + } + + //sync infinite pager adapter with real one + @Override + public void dataSetChanged() { + if (mIsInfiniteAdapter) { + mInfinitePagerAdapter.notifyDataSetChanged(); + mSliderPager.setCurrentItem(0, false); + } + } + + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + // nothing to do + } + + @Override + public void onPageSelected(int position) { + if (mPageListener != null) { + mPageListener.onSliderPageChanged(position); + } + } + + @Override + public void onPageScrollStateChanged(int state) { + // nothing to do + } + + public interface OnSliderPageListener { + + /** + * This method will be invoked when a new page becomes selected. Animation is not + * necessarily complete. + * + * @param position Position index of the new selected page. + */ + void onSliderPageChanged(int position); + + } +} diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/SliderViewAdapter.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/SliderViewAdapter.java new file mode 100644 index 000000000..91e77c1c8 --- /dev/null +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/SliderViewAdapter.java @@ -0,0 +1,89 @@ +package com.smarteist.autoimageslider; + +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.viewpager.widget.PagerAdapter; + +import java.util.LinkedList; +import java.util.Queue; + + +public abstract class SliderViewAdapter extends PagerAdapter { + + private final Queue destroyedItems = new LinkedList<>(); + private DataSetListener dataSetListener; + + @NonNull + @Override + public Object instantiateItem(@NonNull ViewGroup container, int position) { + VH viewHolder = destroyedItems.poll(); + if (viewHolder == null) { + viewHolder = onCreateViewHolder(container); + } + // Re-add existing view before rendering so that we can make change inside getView() + container.addView(viewHolder.itemView); + onBindViewHolder(viewHolder, position); + + return viewHolder; + } + + @Override + public final void destroyItem(ViewGroup container, int position, @NonNull Object object) { + container.removeView(((VH) object).itemView); + destroyedItems.add((VH) object); + } + + @Override + public final boolean isViewFromObject(@NonNull View view, @NonNull Object object) { + return ((VH) object).itemView == view; + } + + @Override + public int getItemPosition(Object object) { + return POSITION_NONE; + } + + @Override + public void notifyDataSetChanged() { + super.notifyDataSetChanged(); + if (this.dataSetListener != null) { + dataSetListener.dataSetChanged(); + } + } + + /** + * Create a new view holder + * + * @param parent wrapper view + * @return view holder + */ + public abstract VH onCreateViewHolder(ViewGroup parent); + + /** + * Bind data at position into viewHolder + * + * @param viewHolder item view holder + * @param position item position + */ + public abstract void onBindViewHolder(VH viewHolder, int position); + + void dataSetChangedListener(SliderViewAdapter.DataSetListener dataSetListener) { + this.dataSetListener = dataSetListener; + } + + interface DataSetListener { + void dataSetChanged(); + } + + //Default View holder class + public static abstract class ViewHolder { + public final View itemView; + + public ViewHolder(View itemView) { + this.itemView = itemView; + } + } + +} diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/AntiClockSpinTransformation.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/AntiClockSpinTransformation.java new file mode 100644 index 000000000..722ff0913 --- /dev/null +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/AntiClockSpinTransformation.java @@ -0,0 +1,41 @@ +package com.smarteist.autoimageslider.Transformations; + +import android.view.View; + +import com.smarteist.autoimageslider.SliderPager; + +public class AntiClockSpinTransformation implements SliderPager.PageTransformer { + @Override + public void transformPage(View page, float position) { + + page.setTranslationX(-position * page.getWidth()); + + if (Math.abs(position) < 0.5) { + page.setVisibility(View.VISIBLE); + page.setScaleX(1 - Math.abs(position)); + page.setScaleY(1 - Math.abs(position)); + } else if (Math.abs(position) > 0.5) { + page.setVisibility(View.GONE); + } + + if (position < -1) { // [-Infinity,-1) + // This page is way off-screen to the left. + page.setAlpha(0); + + } else if (position <= 0) { // [-1,0] + page.setAlpha(1); + page.setRotation(360 * (1 - Math.abs(position))); + + } else if (position <= 1) { // (0,1] + page.setAlpha(1); + page.setRotation(-360 * (1 - Math.abs(position))); + + } else { // (1,+Infinity] + // This page is way off-screen to the right. + page.setAlpha(0); + + } + + + } +} \ No newline at end of file diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/Clock_SpinTransformation.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/Clock_SpinTransformation.java new file mode 100644 index 000000000..002124382 --- /dev/null +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/Clock_SpinTransformation.java @@ -0,0 +1,42 @@ +package com.smarteist.autoimageslider.Transformations; + +import android.view.View; + +import com.smarteist.autoimageslider.SliderPager; + +public class Clock_SpinTransformation implements SliderPager.PageTransformer { + @Override + public void transformPage(View page, float position) { + + page.setTranslationX(-position * page.getWidth()); + + if (Math.abs(position) <= 0.5) { + page.setVisibility(View.VISIBLE); + page.setScaleX(1 - Math.abs(position)); + page.setScaleY(1 - Math.abs(position)); + } else if (Math.abs(position) > 0.5) { + page.setVisibility(View.GONE); + } + + + if (position < -1) { // [-Infinity,-1) + // This page is way off-screen to the left. + page.setAlpha(0); + + } else if (position <= 0) { // [-1,0] + page.setAlpha(1); + page.setRotation(360 * Math.abs(position)); + + } else if (position <= 1) { // (0,1] + page.setAlpha(1); + page.setRotation(-360 * Math.abs(position)); + + } else { // (1,+Infinity] + // This page is way off-screen to the right. + page.setAlpha(0); + + } + + + } +} \ No newline at end of file diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/CubeInDepthTransformation.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/CubeInDepthTransformation.java new file mode 100644 index 000000000..dac027af9 --- /dev/null +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/CubeInDepthTransformation.java @@ -0,0 +1,37 @@ +package com.smarteist.autoimageslider.Transformations; + +import android.view.View; + +import com.smarteist.autoimageslider.SliderPager; + +public class CubeInDepthTransformation implements SliderPager.PageTransformer { + @Override + public void transformPage(View page, float position) { + page.setCameraDistance(20000); + + + if (position < -1) { + page.setAlpha(0); + } else if (position <= 0) { + page.setAlpha(1); + page.setPivotX(page.getWidth()); + page.setRotationY(90 * Math.abs(position)); + } else if (position <= 1) { + page.setAlpha(1); + page.setPivotX(0); + page.setRotationY(-90 * Math.abs(position)); + } else { + page.setAlpha(0); + } + + + if (Math.abs(position) <= 0.5) { + page.setScaleY(Math.max(.4f, 1 - Math.abs(position))); + } else if (Math.abs(position) <= 1) { + page.setScaleY(Math.max(.4f, 1 - Math.abs(position))); + + } + + + } +} \ No newline at end of file diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/CubeInRotationTransformation.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/CubeInRotationTransformation.java new file mode 100644 index 000000000..74d172765 --- /dev/null +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/CubeInRotationTransformation.java @@ -0,0 +1,36 @@ +package com.smarteist.autoimageslider.Transformations; + +import android.view.View; + +import com.smarteist.autoimageslider.SliderPager; + +public class CubeInRotationTransformation implements SliderPager.PageTransformer { + @Override + public void transformPage(View page, float position) { + + page.setCameraDistance(20000); + + + if (position < -1) { // [-Infinity,-1) + // This page is way off-screen to the left. + page.setAlpha(0); + + } else if (position <= 0) { // [-1,0] + page.setAlpha(1); + page.setPivotX(page.getWidth()); + page.setRotationY(90 * Math.abs(position)); + + } else if (position <= 1) { // (0,1] + page.setAlpha(1); + page.setPivotX(0); + page.setRotationY(-90 * Math.abs(position)); + + } else { // (1,+Infinity] + // This page is way off-screen to the right. + page.setAlpha(0); + + } + + + } +} \ No newline at end of file diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/CubeInScalingTransformation.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/CubeInScalingTransformation.java new file mode 100644 index 000000000..8fd92cdbe --- /dev/null +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/CubeInScalingTransformation.java @@ -0,0 +1,43 @@ +package com.smarteist.autoimageslider.Transformations; + +import android.view.View; + +import com.smarteist.autoimageslider.SliderPager; + +public class CubeInScalingTransformation implements SliderPager.PageTransformer { + @Override + public void transformPage(View page, float position) { + page.setCameraDistance(20000); + + + if (position < -1) { // [-Infinity,-1) + // This page is way off-screen to the left. + page.setAlpha(0); + + } else if (position <= 0) { // [-1,0] + page.setAlpha(1); + page.setPivotX(page.getWidth()); + page.setRotationY(90 * Math.abs(position)); + + } else if (position <= 1) { // (0,1] + page.setAlpha(1); + page.setPivotX(0); + page.setRotationY(-90 * Math.abs(position)); + + } else { // (1,+Infinity] + // This page is way off-screen to the right. + page.setAlpha(0); + + } + + + if (Math.abs(position) <= 0.5) { + page.setScaleY(Math.max(.4f, 1 - Math.abs(position))); + } else if (Math.abs(position) <= 1) { + page.setScaleY(Math.max(.4f, Math.abs(position))); + + } + + + } +} \ No newline at end of file diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/CubeOutDepthTransformation.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/CubeOutDepthTransformation.java new file mode 100644 index 000000000..ab459b1b6 --- /dev/null +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/CubeOutDepthTransformation.java @@ -0,0 +1,40 @@ +package com.smarteist.autoimageslider.Transformations; + +import android.view.View; + +import com.smarteist.autoimageslider.SliderPager; + +public class CubeOutDepthTransformation implements SliderPager.PageTransformer { + @Override + public void transformPage(View page, float position) { + + if (position < -1) { // [-Infinity,-1) + // This page is way off-screen to the left. + page.setAlpha(0); + + } else if (position <= 0) { // [-1,0] + page.setAlpha(1); + page.setPivotX(page.getWidth()); + page.setRotationY(-90 * Math.abs(position)); + + } else if (position <= 1) { // (0,1] + page.setAlpha(1); + page.setPivotX(0); + page.setRotationY(90 * Math.abs(position)); + + } else { // (1,+Infinity] + // This page is way off-screen to the right. + page.setAlpha(0); + + } + + + if (Math.abs(position) <= 0.5) { + page.setScaleY(Math.max(0.4f, 1 - Math.abs(position))); + } else if (Math.abs(position) <= 1) { + page.setScaleY(Math.max(0.4f, 1 - Math.abs(position))); + } + + + } +} \ No newline at end of file diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/CubeOutRotationTransformation.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/CubeOutRotationTransformation.java new file mode 100644 index 000000000..69294d6f6 --- /dev/null +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/CubeOutRotationTransformation.java @@ -0,0 +1,31 @@ +package com.smarteist.autoimageslider.Transformations; + +import android.view.View; + +import com.smarteist.autoimageslider.SliderPager; + +public class CubeOutRotationTransformation implements SliderPager.PageTransformer { + @Override + public void transformPage(View page, float position) { + + if (position < -1) { // [-Infinity,-1) + // This page is way off-screen to the left. + page.setAlpha(0); + + } else if (position <= 0) { // [-1,0] + page.setAlpha(1); + page.setPivotX(page.getWidth()); + page.setRotationY(-90 * Math.abs(position)); + + } else if (position <= 1) { // (0,1] + page.setAlpha(1); + page.setPivotX(0); + page.setRotationY(90 * Math.abs(position)); + + } else { // (1,+Infinity] + // This page is way off-screen to the right. + page.setAlpha(0); + + } + } +} \ No newline at end of file diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/CubeOutScalingTransformation.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/CubeOutScalingTransformation.java new file mode 100644 index 000000000..9181b55b5 --- /dev/null +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/CubeOutScalingTransformation.java @@ -0,0 +1,40 @@ +package com.smarteist.autoimageslider.Transformations; + +import android.view.View; + +import com.smarteist.autoimageslider.SliderPager; + +public class CubeOutScalingTransformation implements SliderPager.PageTransformer { + @Override + public void transformPage(View page, float position) { + + if (position < -1) { // [-Infinity,-1) + // This page is way off-screen to the left. + page.setAlpha(0); + + } else if (position <= 0) { // [-1,0] + page.setAlpha(1); + page.setPivotX(page.getWidth()); + page.setRotationY(-90 * Math.abs(position)); + + } else if (position <= 1) { // (0,1] + page.setAlpha(1); + page.setPivotX(0); + page.setRotationY(90 * Math.abs(position)); + + } else { // (1,+Infinity] + // This page is way off-screen to the right. + page.setAlpha(0); + + } + + + if (Math.abs(position) <= 0.5) { + page.setScaleY(Math.max(0.4f, 1 - Math.abs(position))); + } else if (Math.abs(position) <= 1) { + page.setScaleY(Math.max(0.4f, Math.abs(position))); + } + + + } +} \ No newline at end of file diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/DepthTransformation.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/DepthTransformation.java new file mode 100644 index 000000000..b918afc7f --- /dev/null +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/DepthTransformation.java @@ -0,0 +1,35 @@ +package com.smarteist.autoimageslider.Transformations; + +import android.view.View; + +import com.smarteist.autoimageslider.SliderPager; + +public class DepthTransformation implements SliderPager.PageTransformer { + @Override + public void transformPage(View page, float position) { + + if (position < -1) { // [-Infinity,-1) + // This page is way off-screen to the left. + page.setAlpha(0); + + } else if (position <= 0) { // [-1,0] + page.setAlpha(1); + page.setTranslationX(0); + page.setScaleX(1); + page.setScaleY(1); + + } else if (position <= 1) { // (0,1] + page.setTranslationX(-position * page.getWidth()); + page.setAlpha(1 - Math.abs(position)); + page.setScaleX(1 - Math.abs(position)); + page.setScaleY(1 - Math.abs(position)); + + } else { // (1,+Infinity] + // This page is way off-screen to the right. + page.setAlpha(0); + + } + + + } +} \ No newline at end of file diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/FadeTransformation.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/FadeTransformation.java new file mode 100644 index 000000000..d999c9c79 --- /dev/null +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/FadeTransformation.java @@ -0,0 +1,32 @@ +package com.smarteist.autoimageslider.Transformations; + +import android.view.View; + +import com.smarteist.autoimageslider.SliderPager; + +public class FadeTransformation implements SliderPager.PageTransformer { + @Override + public void transformPage(View view, float position) { + + view.setTranslationX(-position * view.getWidth()); + + // Page is not an immediate sibling, just make transparent + if (position < -1 || position > 1) { + view.setAlpha(0); + } + // Page is sibling to left or right + else if (position <= 0 || position <= 1) { + + // Calculate alpha. Position is decimal in [-1,0] or [0,1] + float alpha = (position <= 0) ? position + 1 : 1 - position; + view.setAlpha(alpha); + + } + // Page is active, make fully visible + else if (position == 0) { + view.setAlpha(1); + } + + + } +} \ No newline at end of file diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/FanTransformation.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/FanTransformation.java new file mode 100644 index 000000000..d5ee0138f --- /dev/null +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/FanTransformation.java @@ -0,0 +1,35 @@ +package com.smarteist.autoimageslider.Transformations; + +import android.view.View; + +import com.smarteist.autoimageslider.SliderPager; + +public class FanTransformation implements SliderPager.PageTransformer { + @Override + public void transformPage(View page, float position) { + + page.setTranslationX(-position * page.getWidth()); + page.setPivotX(0); + page.setPivotY(page.getHeight() / 2); + page.setCameraDistance(20000); + + if (position < -1) { // [-Infinity,-1) + // This page is way off-screen to the left. + page.setAlpha(0); + + } else if (position <= 0) { // [-1,0] + page.setAlpha(1); + page.setRotationY(-120 * Math.abs(position)); + } else if (position <= 1) { // (0,1] + page.setAlpha(1); + page.setRotationY(120 * Math.abs(position)); + + } else { // (1,+Infinity] + // This page is way off-screen to the right. + page.setAlpha(0); + + } + + + } +} \ No newline at end of file diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/FidgetSpinTransformation.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/FidgetSpinTransformation.java new file mode 100644 index 000000000..a2b54cbba --- /dev/null +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/FidgetSpinTransformation.java @@ -0,0 +1,42 @@ +package com.smarteist.autoimageslider.Transformations; + +import android.view.View; + +import com.smarteist.autoimageslider.SliderPager; + +public class FidgetSpinTransformation implements SliderPager.PageTransformer { + @Override + public void transformPage(View page, float position) { + + page.setTranslationX(-position * page.getWidth()); + + if (Math.abs(position) < 0.5) { + page.setVisibility(View.VISIBLE); + page.setScaleX(1 - Math.abs(position)); + page.setScaleY(1 - Math.abs(position)); + } else if (Math.abs(position) > 0.5) { + page.setVisibility(View.GONE); + } + + + if (position < -1) { // [-Infinity,-1) + // This page is way off-screen to the left. + page.setAlpha(0); + + } else if (position <= 0) { // [-1,0] + page.setAlpha(1); + page.setRotation(36000 * (Math.abs(position) * Math.abs(position) * Math.abs(position) * Math.abs(position) * Math.abs(position) * Math.abs(position) * Math.abs(position))); + + } else if (position <= 1) { // (0,1] + page.setAlpha(1); + page.setRotation(-36000 * (Math.abs(position) * Math.abs(position) * Math.abs(position) * Math.abs(position) * Math.abs(position) * Math.abs(position) * Math.abs(position))); + + } else { // (1,+Infinity] + // This page is way off-screen to the right. + page.setAlpha(0); + + } + + + } +} \ No newline at end of file diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/GateTransformation.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/GateTransformation.java new file mode 100644 index 000000000..41b612a4e --- /dev/null +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/GateTransformation.java @@ -0,0 +1,39 @@ +package com.smarteist.autoimageslider.Transformations; + +import android.view.View; + +import com.smarteist.autoimageslider.SliderPager; + +public class GateTransformation implements SliderPager.PageTransformer { + + private final String TAG = "GateAnimationn"; + + @Override + public void transformPage(View page, float position) { + + page.setTranslationX(-position * page.getWidth()); + + + if (position < -1) { // [-Infinity,-1) + // This page is way off-screen to the left. + page.setAlpha(0); + + } else if (position <= 0) { // [-1,0] + page.setAlpha(1); + page.setPivotX(0); + page.setRotationY(90 * Math.abs(position)); + + } else if (position <= 1) { // (0,1] + page.setAlpha(1); + page.setPivotX(page.getWidth()); + page.setRotationY(-90 * Math.abs(position)); + + } else { // (1,+Infinity] + // This page is way off-screen to the right. + page.setAlpha(0); + + } + + + } +} \ No newline at end of file diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/HingeTransformation.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/HingeTransformation.java new file mode 100644 index 000000000..63b99f476 --- /dev/null +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/HingeTransformation.java @@ -0,0 +1,36 @@ +package com.smarteist.autoimageslider.Transformations; + +import android.view.View; + +import com.smarteist.autoimageslider.SliderPager; + +public class HingeTransformation implements SliderPager.PageTransformer { + @Override + public void transformPage(View page, float position) { + + page.setTranslationX(-position * page.getWidth()); + page.setPivotX(0); + page.setPivotY(0); + + + if (position < -1) { // [-Infinity,-1) + // This page is way off-screen to the left. + page.setAlpha(0); + + } else if (position <= 0) { // [-1,0] + page.setRotation(90 * Math.abs(position)); + page.setAlpha(1 - Math.abs(position)); + + } else if (position <= 1) { // (0,1] + page.setRotation(0); + page.setAlpha(1); + + } else { // (1,+Infinity] + // This page is way off-screen to the right. + page.setAlpha(0); + + } + + + } +} \ No newline at end of file diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/HorizontalFlipTransformation.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/HorizontalFlipTransformation.java new file mode 100644 index 000000000..897b86574 --- /dev/null +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/HorizontalFlipTransformation.java @@ -0,0 +1,41 @@ +package com.smarteist.autoimageslider.Transformations; + +import android.view.View; + +import com.smarteist.autoimageslider.SliderPager; + +public class HorizontalFlipTransformation implements SliderPager.PageTransformer { + @Override + public void transformPage(View page, float position) { + + page.setTranslationX(-position * page.getWidth()); + page.setCameraDistance(20000); + + if (position < 0.5 && position > -0.5) { + page.setVisibility(View.VISIBLE); + } else { + page.setVisibility(View.INVISIBLE); + } + + + if (position < -1) { // [-Infinity,-1) + // This page is way off-screen to the left. + page.setAlpha(0); + + } else if (position <= 0) { // [-1,0] + page.setAlpha(1); + page.setRotationX(180 * (1 - Math.abs(position) + 1)); + + } else if (position <= 1) { // (0,1] + page.setAlpha(1); + page.setRotationX(-180 * (1 - Math.abs(position) + 1)); + + } else { // (1,+Infinity] + // This page is way off-screen to the right. + page.setAlpha(0); + + } + + + } +} \ No newline at end of file diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/PopTransformation.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/PopTransformation.java new file mode 100644 index 000000000..58a98f597 --- /dev/null +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/PopTransformation.java @@ -0,0 +1,23 @@ +package com.smarteist.autoimageslider.Transformations; + +import android.view.View; + +import com.smarteist.autoimageslider.SliderPager; + +public class PopTransformation implements SliderPager.PageTransformer { + @Override + public void transformPage(View page, float position) { + + page.setTranslationX(-position * page.getWidth()); + + if (Math.abs(position) < 0.5) { + page.setVisibility(View.VISIBLE); + page.setScaleX(1 - Math.abs(position)); + page.setScaleY(1 - Math.abs(position)); + } else if (Math.abs(position) > 0.5) { + page.setVisibility(View.GONE); + } + + + } +} \ No newline at end of file diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/SimpleTransformation.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/SimpleTransformation.java new file mode 100644 index 000000000..5257a0625 --- /dev/null +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/SimpleTransformation.java @@ -0,0 +1,12 @@ +package com.smarteist.autoimageslider.Transformations; + +import android.view.View; + +import com.smarteist.autoimageslider.SliderPager; + +public class SimpleTransformation implements SliderPager.PageTransformer { + @Override + public void transformPage(View page, float position) { + + } +} \ No newline at end of file diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/SpinnerTransformation.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/SpinnerTransformation.java new file mode 100644 index 000000000..f29efbe72 --- /dev/null +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/SpinnerTransformation.java @@ -0,0 +1,41 @@ +package com.smarteist.autoimageslider.Transformations; + +import android.view.View; + +import com.smarteist.autoimageslider.SliderPager; + +public class SpinnerTransformation implements SliderPager.PageTransformer { + @Override + public void transformPage(View page, float position) { + + page.setTranslationX(-position * page.getWidth()); + page.setCameraDistance(12000); + + if (position < 0.5 && position > -0.5) { + page.setVisibility(View.VISIBLE); + } else { + page.setVisibility(View.INVISIBLE); + } + + + if (position < -1) { // [-Infinity,-1) + // This page is way off-screen to the left. + page.setAlpha(0); + + } else if (position <= 0) { // [-1,0] + page.setAlpha(1); + page.setRotationY(900 * (1 - Math.abs(position) + 1)); + + } else if (position <= 1) { // (0,1] + page.setAlpha(1); + page.setRotationY(-900 * (1 - Math.abs(position) + 1)); + + } else { // (1,+Infinity] + // This page is way off-screen to the right. + page.setAlpha(0); + + } + + + } +} \ No newline at end of file diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/TossTransformation.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/TossTransformation.java new file mode 100644 index 000000000..ceb85340b --- /dev/null +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/TossTransformation.java @@ -0,0 +1,48 @@ +package com.smarteist.autoimageslider.Transformations; + +import android.view.View; + +import com.smarteist.autoimageslider.SliderPager; + +public class TossTransformation implements SliderPager.PageTransformer { + @Override + public void transformPage(View page, float position) { + + page.setTranslationX(-position * page.getWidth()); + page.setCameraDistance(20000); + + + if (position < 0.5 && position > -0.5) { + page.setVisibility(View.VISIBLE); + + } else { + page.setVisibility(View.INVISIBLE); + + } + + + if (position < -1) { // [-Infinity,-1) + // This page is way off-screen to the left. + page.setAlpha(0); + + } else if (position <= 0) { // [-1,0] + page.setAlpha(1); + page.setScaleX(Math.max(0.4f, (1 - Math.abs(position)))); + page.setScaleY(Math.max(0.4f, (1 - Math.abs(position)))); + page.setRotationX(1080 * (1 - Math.abs(position) + 1)); + page.setTranslationY(-1000 * Math.abs(position)); + + } else if (position <= 1) { // (0,1] + page.setAlpha(1); + page.setScaleX(Math.max(0.4f, (1 - Math.abs(position)))); + page.setScaleY(Math.max(0.4f, (1 - Math.abs(position)))); + page.setRotationX(-1080 * (1 - Math.abs(position) + 1)); + page.setTranslationY(-1000 * Math.abs(position)); + + } else { // (1,+Infinity] + // This page is way off-screen to the right. + page.setAlpha(0); + + } + } +} \ No newline at end of file diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/VerticalFlipTransformation.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/VerticalFlipTransformation.java new file mode 100644 index 000000000..ae3b0144f --- /dev/null +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/VerticalFlipTransformation.java @@ -0,0 +1,41 @@ +package com.smarteist.autoimageslider.Transformations; + +import android.view.View; + +import com.smarteist.autoimageslider.SliderPager; + +public class VerticalFlipTransformation implements SliderPager.PageTransformer { + @Override + public void transformPage(View page, float position) { + + page.setTranslationX(-position * page.getWidth()); + page.setCameraDistance(12000); + + if (position < 0.5 && position > -0.5) { + page.setVisibility(View.VISIBLE); + } else { + page.setVisibility(View.INVISIBLE); + } + + + if (position < -1) { // [-Infinity,-1) + // This page is way off-screen to the left. + page.setAlpha(0); + + } else if (position <= 0) { // [-1,0] + page.setAlpha(1); + page.setRotationY(180 * (1 - Math.abs(position) + 1)); + + } else if (position <= 1) { // (0,1] + page.setAlpha(1); + page.setRotationY(-180 * (1 - Math.abs(position) + 1)); + + } else { // (1,+Infinity] + // This page is way off-screen to the right. + page.setAlpha(0); + + } + + + } +} \ No newline at end of file diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/VerticalShutTransformation.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/VerticalShutTransformation.java new file mode 100644 index 000000000..b4fc2e8b1 --- /dev/null +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/VerticalShutTransformation.java @@ -0,0 +1,41 @@ +package com.smarteist.autoimageslider.Transformations; + +import android.view.View; + +import com.smarteist.autoimageslider.SliderPager; + +public class VerticalShutTransformation implements SliderPager.PageTransformer { + @Override + public void transformPage(View page, float position) { + + page.setTranslationX(-position * page.getWidth()); + page.setCameraDistance(999999999); + + if (position < 0.5 && position > -0.5) { + page.setVisibility(View.VISIBLE); + } else { + page.setVisibility(View.INVISIBLE); + } + + + if (position < -1) { // [-Infinity,-1) + // This page is way off-screen to the left. + page.setAlpha(0); + + } else if (position <= 0) { // [-1,0] + page.setAlpha(1); + page.setRotationX(180 * (1 - Math.abs(position) + 1)); + + } else if (position <= 1) { // (0,1] + page.setAlpha(1); + page.setRotationX(-180 * (1 - Math.abs(position) + 1)); + + } else { // (1,+Infinity] + // This page is way off-screen to the right. + page.setAlpha(0); + + } + + + } +} \ No newline at end of file diff --git a/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/ZoomOutTransformation.java b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/ZoomOutTransformation.java new file mode 100644 index 000000000..ad5558b81 --- /dev/null +++ b/autoimageslider/src/main/java/com/smarteist/autoimageslider/Transformations/ZoomOutTransformation.java @@ -0,0 +1,33 @@ +package com.smarteist.autoimageslider.Transformations; + +import android.view.View; + +import com.smarteist.autoimageslider.SliderPager; + +public class ZoomOutTransformation implements SliderPager.PageTransformer { + + private static final float MIN_SCALE = 0.65f; + private static final float MIN_ALPHA = 0.3f; + + @Override + public void transformPage(View page, float position) { + + if (position < -1) { // [-Infinity,-1) + // This page is way off-screen to the left. + page.setAlpha(0); + + } else if (position <= 1) { // [-1,1] + + page.setScaleX(Math.max(MIN_SCALE, 1 - Math.abs(position))); + page.setScaleY(Math.max(MIN_SCALE, 1 - Math.abs(position))); + page.setAlpha(Math.max(MIN_ALPHA, 1 - Math.abs(position))); + + } else { // (1,+Infinity] + // This page is way off-screen to the right. + page.setAlpha(0); + + } + + + } +} \ No newline at end of file diff --git a/autoimageslider/src/main/res/values/attrs.xml b/autoimageslider/src/main/res/values/attrs.xml new file mode 100644 index 000000000..4ccc851c3 --- /dev/null +++ b/autoimageslider/src/main/res/values/attrs.xml @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/autoimageslider/src/main/res/values/strings.xml b/autoimageslider/src/main/res/values/strings.xml new file mode 100644 index 000000000..b574c120e --- /dev/null +++ b/autoimageslider/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + AutoImageSlider + diff --git a/autoimageslider/src/test/java/com/smarteist/autoimageslider/ExampleUnitTest.java b/autoimageslider/src/test/java/com/smarteist/autoimageslider/ExampleUnitTest.java new file mode 100644 index 000000000..48b6d5ea4 --- /dev/null +++ b/autoimageslider/src/test/java/com/smarteist/autoimageslider/ExampleUnitTest.java @@ -0,0 +1,17 @@ +package com.smarteist.autoimageslider; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + @Test + public void addition_isCorrect() { + assertEquals(4, 2 + 2); + } +} \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 000000000..7d4853146 --- /dev/null +++ b/build.gradle @@ -0,0 +1,24 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +buildscript { + repositories { + mavenCentral() + google() + } + dependencies { + classpath 'com.android.tools.build:gradle:7.0.4' + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +allprojects { + repositories { + mavenCentral() + google() + maven { url 'https://jitpack.io' } + } +} +task clean(type: Delete) { + delete rootProject.buildDir +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 000000000..52f5917cb --- /dev/null +++ b/gradle.properties @@ -0,0 +1,19 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app"s APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Automatically convert third-party libraries to use AndroidX +android.enableJetifier=true \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..e708b1c02 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..dbef1cdf8 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Wed Nov 03 09:29:30 CET 2021 +distributionBase=GRADLE_USER_HOME +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip +distributionPath=wrapper/dists +zipStorePath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew new file mode 100644 index 000000000..4f906e0c8 --- /dev/null +++ b/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 000000000..107acd32c --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/mytransl/.gitignore b/mytransl/.gitignore new file mode 100644 index 000000000..796b96d1c --- /dev/null +++ b/mytransl/.gitignore @@ -0,0 +1 @@ +/build diff --git a/mytransl/build.gradle b/mytransl/build.gradle new file mode 100644 index 000000000..6cd6f921a --- /dev/null +++ b/mytransl/build.gradle @@ -0,0 +1,34 @@ +apply plugin: 'com.android.library' + +group = 'com.github.stom79' +android { + compileSdkVersion 31 + + + defaultConfig { + minSdkVersion 15 + targetSdkVersion 31 + versionCode 7 + versionName "3.0" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + buildConfigField("String","VERSION_NAME","\"${defaultConfig.versionName}\"") + } + debug{ + buildConfigField("String","VERSION_NAME","\"${defaultConfig.versionName}\"") + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation 'androidx.legacy:legacy-support-v4:1.0.0' + implementation "com.google.code.gson:gson:2.8.6" +} diff --git a/mytransl/src/main/AndroidManifest.xml b/mytransl/src/main/AndroidManifest.xml new file mode 100644 index 000000000..c3342ad18 --- /dev/null +++ b/mytransl/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + + diff --git a/mytransl/src/main/java/com/github/stom79/mytransl/MyTransL.java b/mytransl/src/main/java/com/github/stom79/mytransl/MyTransL.java new file mode 100644 index 000000000..4f96bc5c0 --- /dev/null +++ b/mytransl/src/main/java/com/github/stom79/mytransl/MyTransL.java @@ -0,0 +1,150 @@ +package com.github.stom79.mytransl; +/* Copyright 2017 Thomas Schneider + * + * This file is a part of MyTransL + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * MyTransL is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with MyTransL; if not, + * see . */ + + +import com.github.stom79.mytransl.async.TransAsync; +import com.github.stom79.mytransl.client.Results; +import com.github.stom79.mytransl.translate.Params; + +import java.util.Locale; + + +@SuppressWarnings({"unused", "RedundantSuppression"}) +public class MyTransL { + + public static String TAG = "MyTrans_TAG"; + private static MyTransL myTransL; + private static String libretranslateDomain; + private final translatorEngine te; + private String yandexAPIKey, deeplAPIKey, systranAPIKey, libreTranslateAPIKey; + private int timeout = 30; + private boolean obfuscation = false; + + private MyTransL(translatorEngine te) { + this.te = te; + } + + public static synchronized MyTransL getInstance(translatorEngine te) { + if (myTransL == null) + myTransL = new MyTransL(te); + return myTransL; + } + + /** + * Allows to get the current locale of the device + * + * @return locale String + */ + public static String getLibreTranslateUrl() { + return "https://" + libretranslateDomain + "/translate?"; + } + + /** + * Allows to get the current locale of the device + * + * @return locale String + */ + public static String getLocale() { + return Locale.getDefault().getLanguage(); + } + + /** + * Timeout in seconds + * + * @param timeout - int + */ + public void setTimeout(int timeout) { + this.timeout = timeout; + } + + public void setObfuscation(boolean obfuscation) { + this.obfuscation = obfuscation; + } + + public boolean isObfuscated() { + return this.obfuscation; + } + + public String getDeeplAPIKey() { + return deeplAPIKey; + } + + public void setDeeplAPIKey(String deeplAPIKey) { + this.deeplAPIKey = deeplAPIKey; + } + + public String getYandexAPIKey() { + return this.yandexAPIKey; + } + + public void setYandexAPIKey(String key) { + this.yandexAPIKey = key; + } + + public String getSystranAPIKey() { + return this.systranAPIKey; + } + + public void setSystranAPIKey(String key) { + this.systranAPIKey = key; + } + + public String getLibretranslateDomain() { + return libretranslateDomain; + } + + public void setLibretranslateDomain(String libretranslateDomain) { + MyTransL.libretranslateDomain = libretranslateDomain; + } + + public String getLibreTranslateAPIKey() { + return libreTranslateAPIKey; + } + + public void setLibreTranslateAPIKey(String libreTranslateAPIKey) { + this.libreTranslateAPIKey = libreTranslateAPIKey; + } + + /** + * Asynchronous call for the translation + * + * @param content String - Content to translate + * @param toLanguage - String the targeted language + * @param listener - Callback for the asynchronous call + */ + public void translate(final String content, final String toLanguage, Params params, final Results listener) { + new TransAsync(te, content, toLanguage, params, timeout, obfuscation, listener); + } + + /** + * Asynchronous call for the translation + * + * @param content String - Content to translate + * @param toLanguage - String the targeted language + * @param listener - Callback for the asynchronous call + */ + public void translate(final String content, Params.fType format, final String toLanguage, final Results listener) { + new TransAsync(te, content, format, toLanguage, timeout, obfuscation, listener); + } + + public enum translatorEngine { + YANDEX, + DEEPL, + SYSTRAN, + LIBRETRANSLATE + } + +} diff --git a/mytransl/src/main/java/com/github/stom79/mytransl/async/TransAsync.java b/mytransl/src/main/java/com/github/stom79/mytransl/async/TransAsync.java new file mode 100644 index 000000000..7c9052199 --- /dev/null +++ b/mytransl/src/main/java/com/github/stom79/mytransl/async/TransAsync.java @@ -0,0 +1,181 @@ +package com.github.stom79.mytransl.async; +/* Copyright 2017 Thomas Schneider + * + * This file is a part of MyTransL + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * MyTransL is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with MyTransL; if not, + * see . */ + +import android.os.Handler; +import android.os.Looper; + +import com.github.stom79.mytransl.MyTransL; +import com.github.stom79.mytransl.client.Client; +import com.github.stom79.mytransl.client.HttpsConnectionException; +import com.github.stom79.mytransl.client.Results; +import com.github.stom79.mytransl.translate.Helper; +import com.github.stom79.mytransl.translate.Params; +import com.github.stom79.mytransl.translate.Translate; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; + + +/** + * Created by @stom79 on 27/11/2017. + * Asynchronous task to get the translation + * Changed 10/01/2021 + */ + +public class TransAsync { + + private final Results listener; + private final MyTransL.translatorEngine te; + private final int timeout; + private final Translate translate; + private final boolean obfuscation; + private final String contentToSend; + private final String toLanguage; + private final Params params; + private Params.fType format; + private HttpsConnectionException e; + + public TransAsync(MyTransL.translatorEngine te, String content, Params.fType format, String toLanguage, int timeout, boolean obfuscation, Results results) { + this.listener = results; + this.te = te; + this.timeout = timeout; + this.obfuscation = obfuscation; + //An instance of the Translate class will be hydrated depending of the translator engine + translate = new Translate(); + translate.setTranslatorEngine(te); + translate.setInitialContent(content); + translate.setTargetedLanguage(toLanguage); + translate.setFormat(format); + //Obfuscation if asked + if (obfuscation) + translate.obfuscate(); + if (obfuscation) { + contentToSend = translate.getObfuscateContent(); + } else { + contentToSend = translate.getInitialContent(); + } + this.toLanguage = toLanguage; + this.params = new Params(); + + new Thread(() -> { + String response = doInBackground(); + Handler mainHandler = new Handler(Looper.getMainLooper()); + Runnable myRunnable = () -> onPostExecute(response); + mainHandler.post(myRunnable); + }).start(); + } + + public TransAsync(MyTransL.translatorEngine te, String content, String toLanguage, Params params, int timeout, boolean obfuscation, Results results) { + this.listener = results; + this.te = te; + this.timeout = timeout; + this.obfuscation = obfuscation; + //An instance of the Translate class will be hydrated depending of the translator engine + translate = new Translate(); + translate.setTranslatorEngine(te); + translate.setInitialContent(content); + translate.setTargetedLanguage(toLanguage); + //Obfuscation if asked + if (obfuscation) { + translate.obfuscate(); + } + if (obfuscation) { + contentToSend = translate.getObfuscateContent(); + } else { + contentToSend = translate.getInitialContent(); + } + this.toLanguage = toLanguage; + this.params = params; + new Thread(() -> { + String response = doInBackground(); + Handler mainHandler = new Handler(Looper.getMainLooper()); + MyTransL.getLocale(); + Runnable myRunnable = () -> onPostExecute(response); + mainHandler.post(myRunnable); + }).start(); + } + + + protected String doInBackground() { + String str_response = null; + //Some parameters + try { + String url; + + if (te == MyTransL.translatorEngine.YANDEX) { + String key = MyTransL.getInstance(te).getYandexAPIKey(); + url = Helper.getYandexAbsoluteUrl(contentToSend, key, toLanguage); + str_response = new Client().get(url, this.timeout); + } else if (te == MyTransL.translatorEngine.DEEPL) { + String key = MyTransL.getInstance(te).getDeeplAPIKey(); + url = Helper.getDeeplAbsoluteUrl(contentToSend, toLanguage, params, key); + str_response = new Client().get(url, this.timeout); + } else if (te == MyTransL.translatorEngine.SYSTRAN) { + String key = MyTransL.getInstance(te).getSystranAPIKey(); + url = Helper.getSystranAbsoluteUrl(contentToSend, key, toLanguage); + str_response = new Client().get(url, this.timeout); + } else if (te == MyTransL.translatorEngine.LIBRETRANSLATE) { + String key = MyTransL.getInstance(te).getLibreTranslateAPIKey(); + JSONObject params = new JSONObject(); + try { + params.put("source", this.params.getSource_lang()); + params.put("target", toLanguage); + params.put("q", contentToSend); + params.put("format", format); + if (key != null) { + params.put("key", key); + } + } catch (JSONException e) { + e.printStackTrace(); + } + str_response = new Client().post(MyTransL.getLibreTranslateUrl(), this.timeout, params); + } + } catch (IOException | NoSuchAlgorithmException | KeyManagementException err) { + this.e = new HttpsConnectionException(-1, err.getMessage()); + err.printStackTrace(); + } catch (HttpsConnectionException e) { + this.e = e; + } + return str_response; + } + + protected void onPostExecute(String result) { + if (this.e == null) { + //Yandex response + if (this.te == MyTransL.translatorEngine.YANDEX) { + translate.parseYandexResult(result, listener); + } else if (this.te == MyTransL.translatorEngine.DEEPL) { + translate.parseDeeplResult(result, listener); + } else if (this.te == MyTransL.translatorEngine.SYSTRAN) { + translate.parseSystranlResult(result, listener); + } else if (this.te == MyTransL.translatorEngine.LIBRETRANSLATE) { + translate.parseLibreTranslateResult(result, listener); + } + //Obfuscation if asked + if (obfuscation) { + translate.deobfuscate(); + } + listener.onSuccess(translate); + } else { + listener.onFail(this.e); + } + } + +} diff --git a/mytransl/src/main/java/com/github/stom79/mytransl/client/Client.java b/mytransl/src/main/java/com/github/stom79/mytransl/client/Client.java new file mode 100644 index 000000000..26826622d --- /dev/null +++ b/mytransl/src/main/java/com/github/stom79/mytransl/client/Client.java @@ -0,0 +1,171 @@ +package com.github.stom79.mytransl.client; +/* Copyright 2017 Thomas Schneider + * + * This file is a part of MyTransL + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * MyTransL is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with MyTransL; if not, + * see . */ + +import android.os.Build; + +import com.github.stom79.mytransl.BuildConfig; + +import org.json.JSONObject; + +import java.io.BufferedReader; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; + +import javax.net.ssl.HttpsURLConnection; + +/** + * Created by @stom79 on 27/11/2017. + * Manages GET and POST calls + * Changed 10/01/2021 + */ + +public class Client { + + + private static final String USER_AGENT = "MyTransL/" + BuildConfig.VERSION_NAME + " Android/" + Build.VERSION.RELEASE; + + public Client() { + } + + + /*** + * Get call to the translator API + * @param urlConnection - String url to query + * @param timeout - int a timeout + * @return response - String + * @throws IOException - Exception + * @throws NoSuchAlgorithmException - Exception + * @throws KeyManagementException - Exception + * @throws HttpsConnectionException - Exception + */ + @SuppressWarnings({"SameParameterValue"}) + public String get(String urlConnection, int timeout) throws IOException, NoSuchAlgorithmException, KeyManagementException, HttpsConnectionException { + URL url = new URL(urlConnection); + HttpsURLConnection httpsURLConnection = (HttpsURLConnection) url.openConnection(); + httpsURLConnection.setConnectTimeout(timeout * 1000); + httpsURLConnection.setRequestProperty("http.keepAlive", "false"); + httpsURLConnection.setRequestProperty("User-Agent", USER_AGENT); + if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.LOLLIPOP) + httpsURLConnection.setSSLSocketFactory(new TLSSocketFactory()); + httpsURLConnection.setRequestMethod("GET"); + //Read the reply + if (httpsURLConnection.getResponseCode() >= 200 && httpsURLConnection.getResponseCode() < 400) { + Reader in; + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) { + in = new BufferedReader(new InputStreamReader(httpsURLConnection.getInputStream(), StandardCharsets.UTF_8)); + } else { + //noinspection CharsetObjectCanBeUsed + in = new BufferedReader(new InputStreamReader(httpsURLConnection.getInputStream(), "UTF-8")); + } + StringBuilder sb = new StringBuilder(); + for (int c; (c = in.read()) >= 0; ) + sb.append((char) c); + httpsURLConnection.disconnect(); + in.close(); + return sb.toString(); + } else { + Reader in; + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) { + in = new BufferedReader(new InputStreamReader(httpsURLConnection.getErrorStream(), StandardCharsets.UTF_8)); + } else { + //noinspection CharsetObjectCanBeUsed + in = new BufferedReader(new InputStreamReader(httpsURLConnection.getErrorStream(), "UTF-8")); + } + StringBuilder sb = new StringBuilder();// TODO Auto-generated catch block + for (int c; (c = in.read()) >= 0; ) + sb.append((char) c); + httpsURLConnection.disconnect(); + throw new HttpsConnectionException(httpsURLConnection.getResponseCode(), sb.toString()); + } + } + + + /*** + * POST call to the translator API + * @param urlConnection - String url to query + * @param timeout - int a timeout + * @param jsonObject - parameters to send (JSON) + * @return response - String + * @throws IOException - Exception + * @throws NoSuchAlgorithmException - Exception + * @throws KeyManagementException - Exception + * @throws HttpsConnectionException - Exception + */ + @SuppressWarnings({"SameParameterValue", "unused", "RedundantSuppression"}) + public String post(String urlConnection, int timeout, JSONObject jsonObject) throws IOException, NoSuchAlgorithmException, KeyManagementException, HttpsConnectionException { + URL url = new URL(urlConnection); + HttpsURLConnection httpsURLConnection = (HttpsURLConnection) url.openConnection(); + byte[] postDataBytes; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + postDataBytes = jsonObject.toString().getBytes(StandardCharsets.UTF_8); + } else { + //noinspection CharsetObjectCanBeUsed + postDataBytes = jsonObject.toString().getBytes("utf-8"); + } + httpsURLConnection.setRequestProperty("User-Agent", USER_AGENT); + httpsURLConnection.setConnectTimeout(timeout * 1000); + httpsURLConnection.setDoInput(true); + httpsURLConnection.setDoOutput(true); + httpsURLConnection.setUseCaches(false); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) + httpsURLConnection.setSSLSocketFactory(new TLSSocketFactory()); + httpsURLConnection.setRequestMethod("POST"); + httpsURLConnection.setRequestProperty("Content-Type", "application/json"); + httpsURLConnection.setRequestProperty("Accept", "application/json"); + httpsURLConnection.setRequestProperty("Content-Length", String.valueOf(postDataBytes.length)); + // Send POST output + DataOutputStream printout = new DataOutputStream(httpsURLConnection.getOutputStream()); + httpsURLConnection.getOutputStream().write(postDataBytes); + printout.flush(); + printout.close(); + //Read the reply + if (httpsURLConnection.getResponseCode() >= 200 && httpsURLConnection.getResponseCode() < 400) { + Reader in; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + in = new BufferedReader(new InputStreamReader(httpsURLConnection.getInputStream(), StandardCharsets.UTF_8)); + } else { + //noinspection CharsetObjectCanBeUsed + in = new BufferedReader(new InputStreamReader(httpsURLConnection.getInputStream(), "UTF-8")); + } + StringBuilder sb = new StringBuilder(); + for (int c; (c = in.read()) >= 0; ) + sb.append((char) c); + httpsURLConnection.disconnect(); + in.close(); + return sb.toString(); + } else { + Reader in; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + in = new BufferedReader(new InputStreamReader(httpsURLConnection.getErrorStream(), StandardCharsets.UTF_8)); + } else { + //noinspection CharsetObjectCanBeUsed + in = new BufferedReader(new InputStreamReader(httpsURLConnection.getErrorStream(), "UTF-8")); + } + StringBuilder sb = new StringBuilder(); + for (int c; (c = in.read()) >= 0; ) + sb.append((char) c); + httpsURLConnection.disconnect(); + throw new HttpsConnectionException(httpsURLConnection.getResponseCode(), sb.toString()); + } + } + +} diff --git a/mytransl/src/main/java/com/github/stom79/mytransl/client/HttpsConnectionException.java b/mytransl/src/main/java/com/github/stom79/mytransl/client/HttpsConnectionException.java new file mode 100644 index 000000000..fd4adebfe --- /dev/null +++ b/mytransl/src/main/java/com/github/stom79/mytransl/client/HttpsConnectionException.java @@ -0,0 +1,52 @@ +package com.github.stom79.mytransl.client; +/* Copyright 2017 Thomas Schneider + * + * This file is a part of MyTransL + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * MyTransL is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with MyTransL; if not, + * see . */ + +import android.os.Build; +import android.text.Html; +import android.text.SpannableString; + +/** + * Created by @stom79 on 28/11/2017. + * Manage custom Exception + * Changed 10/01/2021 + */ + +@SuppressWarnings({"unused", "RedundantSuppression"}) +public class HttpsConnectionException extends Exception { + + private final int statusCode; + private final String message; + + public HttpsConnectionException(int statusCode, String message) { + this.statusCode = statusCode; + SpannableString spannableString; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + spannableString = new SpannableString(Html.fromHtml(message, Html.FROM_HTML_MODE_LEGACY)); + } else { + spannableString = new SpannableString(Html.fromHtml(message)); + } + this.message = spannableString.toString(); + } + + public int getStatusCode() { + return statusCode; + } + + @Override + public String getMessage() { + return message; + } +} diff --git a/mytransl/src/main/java/com/github/stom79/mytransl/client/Results.java b/mytransl/src/main/java/com/github/stom79/mytransl/client/Results.java new file mode 100644 index 000000000..28cfce2b9 --- /dev/null +++ b/mytransl/src/main/java/com/github/stom79/mytransl/client/Results.java @@ -0,0 +1,29 @@ +package com.github.stom79.mytransl.client; +/* Copyright 2017 Thomas Schneider + * + * This file is a part of MyTransL + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * MyTransL is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with MyTransL; if not, + * see . */ + + +import com.github.stom79.mytransl.translate.Translate; + +/** + * Created by @stom79 on 27/11/2017. + * Handler for the results of the translation + */ + +public interface Results { + void onSuccess(Translate translate); + + void onFail(HttpsConnectionException httpsConnectionException); +} diff --git a/mytransl/src/main/java/com/github/stom79/mytransl/client/TLSSocketFactory.java b/mytransl/src/main/java/com/github/stom79/mytransl/client/TLSSocketFactory.java new file mode 100644 index 000000000..8b9f665a3 --- /dev/null +++ b/mytransl/src/main/java/com/github/stom79/mytransl/client/TLSSocketFactory.java @@ -0,0 +1,91 @@ +package com.github.stom79.mytransl.client; +/* Copyright 2017 Thomas Schneider + * + * This file is a part of MyTransL + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * MyTransL is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with MyTransL; if not, + * see . */ + + +import java.io.IOException; +import java.net.InetAddress; +import java.net.Socket; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; + +/** + * Created by @stom79 on 27/11/2017. + * Enable TLS 1.1 & 1.2 on older devices + * Changed 10/01/2021 + */ + +public class TLSSocketFactory extends SSLSocketFactory { + + private final SSLSocketFactory sSLSocketFactory; + + TLSSocketFactory() throws KeyManagementException, NoSuchAlgorithmException { + + SSLContext context = SSLContext.getInstance("TLS"); + context.init(null, null, null); + sSLSocketFactory = context.getSocketFactory(); + } + + @Override + public String[] getDefaultCipherSuites() { + return sSLSocketFactory.getDefaultCipherSuites(); + } + + @Override + public String[] getSupportedCipherSuites() { + return sSLSocketFactory.getSupportedCipherSuites(); + } + + @Override + public Socket createSocket() throws IOException { + return enableTLSOnSocket(sSLSocketFactory.createSocket()); + } + + @Override + public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException { + return enableTLSOnSocket(sSLSocketFactory.createSocket(s, host, port, autoClose)); + } + + @Override + public Socket createSocket(String host, int port) throws IOException { + return enableTLSOnSocket(sSLSocketFactory.createSocket(host, port)); + } + + @Override + public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException { + return enableTLSOnSocket(sSLSocketFactory.createSocket(host, port, localHost, localPort)); + } + + @Override + public Socket createSocket(InetAddress host, int port) throws IOException { + return enableTLSOnSocket(sSLSocketFactory.createSocket(host, port)); + } + + @Override + public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException { + return enableTLSOnSocket(sSLSocketFactory.createSocket(address, port, localAddress, localPort)); + } + + private Socket enableTLSOnSocket(Socket socket) { + if ((socket instanceof SSLSocket)) { + ((SSLSocket) socket).setEnabledProtocols(new String[]{"TLSv1.1", "TLSv1.2",}); + } + return socket; + } +} diff --git a/mytransl/src/main/java/com/github/stom79/mytransl/translate/Helper.java b/mytransl/src/main/java/com/github/stom79/mytransl/translate/Helper.java new file mode 100644 index 000000000..86aaac0f6 --- /dev/null +++ b/mytransl/src/main/java/com/github/stom79/mytransl/translate/Helper.java @@ -0,0 +1,129 @@ +package com.github.stom79.mytransl.translate; +/* Copyright 2017 Thomas Schneider + * + * This file is a part of MyTransL + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * MyTransL is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with MyTransL; if not, + * see . */ + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.Arrays; + +/** + * Created by Thomas on 28/11/2017. + * Some static references + * Changed 10/01/2021 + */ + +public class Helper { + + + private static final String YANDEX_BASE_URL = "https://translate.yandex.net/api/v1.5/tr.json/translate?"; + private static final String DEEPL_BASE_URL = "https://api.deepl.com/v1/translate?"; + private static final String SYSTRAN_BASE_URL = "https://api-platform.systran.net/translation/text/translate?"; + private static final String[] deeplAvailableLang = {"EN", "DE", "FR", "ES", "IT", "NL", "PL"}; + + + /*** + * Returns the URL for Yandex + * @param content String - Content to translate + * @param apikey String - The Yandex API Key + * @param toLanguage String - The targeted locale + * @return String - absolute URL for Yandex + */ + public static String getYandexAbsoluteUrl(String content, String apikey, String toLanguage) { + String key = "key=" + apikey + "&"; + toLanguage = toLanguage.replace("null", ""); + String lang = "lang=" + toLanguage + "&"; + String text; + try { + text = "text=" + URLEncoder.encode(content, "utf-8") + "&"; + } catch (UnsupportedEncodingException e) { + text = "text=" + content + "&"; + e.printStackTrace(); + } + String format = "format=html&"; + return Helper.YANDEX_BASE_URL + key + lang + format + text; + } + + + /*** + * Returns the URL for Deepl + * @param content String - Content to translate + * @param toLanguage String - The targeted locale + * @param deepLParams DeepLParams - The deepl paramaters see: https://www.deepl.com/api.html#api_reference_article + * @param apikey String - The Deepl API Key + * @return String - absolute URL for Deepl + */ + public static String getDeeplAbsoluteUrl(String content, String toLanguage, Params deepLParams, String apikey) { + String key = "&auth_key=" + apikey; + toLanguage = toLanguage.replace("null", ""); + String lang = "target_lang=" + toLanguage.toUpperCase(); + String text; + try { + text = "text=" + URLEncoder.encode(content, "utf-8") + "&"; + } catch (UnsupportedEncodingException e) { + text = "text=" + content + "&"; + e.printStackTrace(); + } + String params = ""; + if (deepLParams.isPreserve_formatting()) + params += "&preserve_formatting=1"; + else + params += "&preserve_formatting=0"; + + if (deepLParams.isSplit_sentences()) + params += "&split_sentences=1"; + else + params += "&split_sentences=0"; + + if (deepLParams.getSource_lang() != null && Arrays.asList(deeplAvailableLang).contains(deepLParams.getSource_lang().toUpperCase())) + params += "&split_sentences=" + deepLParams.getSource_lang(); + + if (deepLParams.getIgnore_tags() != null) + params += "&ignore_tags=" + deepLParams.getIgnore_tags(); + + if (deepLParams.getTag_handling() != null) + params += "&tag_handling=" + deepLParams.getTag_handling(); + + if (deepLParams.getNon_splitting_tags() != null) + params += "&tag_handling=" + deepLParams.getNon_splitting_tags(); + + + return Helper.DEEPL_BASE_URL + text + lang + params + key; + } + + + /*** + * Returns the URL for Systran + * @param content String - Content to translate + * @param toLanguage String - The targeted locale + * @param apikey String - The Systran API Key + * @return String - absolute URL for Systran + */ + public static String getSystranAbsoluteUrl(String content, String apikey, String toLanguage) { + String key = "key=" + apikey + "&"; + String from = "source=auto&"; + toLanguage = toLanguage.replace("null", ""); + String lang = "target=" + toLanguage + "&"; + String text; + try { + text = "input=" + URLEncoder.encode(content, "utf-8") + "&"; + } catch (UnsupportedEncodingException e) { + text = "input=" + content + "&"; + e.printStackTrace(); + } + String encoding = "encoding=utf-8&"; + return Helper.SYSTRAN_BASE_URL + key + from + lang + encoding + text; + } + +} diff --git a/mytransl/src/main/java/com/github/stom79/mytransl/translate/Params.java b/mytransl/src/main/java/com/github/stom79/mytransl/translate/Params.java new file mode 100644 index 000000000..fba8eec63 --- /dev/null +++ b/mytransl/src/main/java/com/github/stom79/mytransl/translate/Params.java @@ -0,0 +1,104 @@ +package com.github.stom79.mytransl.translate; +/* Copyright 2018 Thomas Schneider + * + * This file is a part of MyTransL + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * MyTransL is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with MyTransL; if not, + * see . */ + +import com.google.gson.annotations.SerializedName; + +@SuppressWarnings({"unused", "RedundantSuppression"}) +public class Params { + + private String source_lang; + private String tag_handling; + private String non_splitting_tags; + private String ignore_tags; + private boolean split_sentences = true; + private boolean preserve_formatting = false; + private Params.fType format; + + public Params.fType getFormat() { + if (format == null) { + format = Params.fType.TEXT; + } + return format; + } + + public void setFormat(Params.fType format) { + this.format = format; + } + + public String getSource_lang() { + return source_lang; + } + + public void setSource_lang(String source_lang) { + this.source_lang = source_lang; + } + + public String getTag_handling() { + return tag_handling; + } + + public void setTag_handling(String tag_handling) { + this.tag_handling = tag_handling; + } + + public String getNon_splitting_tags() { + return non_splitting_tags; + } + + public void setNon_splitting_tags(String non_splitting_tags) { + this.non_splitting_tags = non_splitting_tags; + } + + public String getIgnore_tags() { + return ignore_tags; + } + + public void setIgnore_tags(String ignore_tags) { + this.ignore_tags = ignore_tags; + } + + public boolean isSplit_sentences() { + return split_sentences; + } + + public void setSplit_sentences(boolean split_sentences) { + this.split_sentences = split_sentences; + } + + public boolean isPreserve_formatting() { + return preserve_formatting; + } + + public void setPreserve_formatting(boolean preserve_formatting) { + this.preserve_formatting = preserve_formatting; + } + + public enum fType { + @SerializedName("TEXT") + TEXT("text"), + @SerializedName("HTML") + HTML("html"); + private final String value; + + fType(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + } +} diff --git a/mytransl/src/main/java/com/github/stom79/mytransl/translate/Translate.java b/mytransl/src/main/java/com/github/stom79/mytransl/translate/Translate.java new file mode 100644 index 000000000..6bccabff0 --- /dev/null +++ b/mytransl/src/main/java/com/github/stom79/mytransl/translate/Translate.java @@ -0,0 +1,388 @@ +package com.github.stom79.mytransl.translate; +/* Copyright 2017 Thomas Schneider + * + * This file is a part of MyTransL + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * MyTransL is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with MyTransL; if not, + * see . */ + + +import android.os.Build; +import android.text.Html; +import android.text.SpannableString; +import android.util.Patterns; + +import com.github.stom79.mytransl.MyTransL; +import com.github.stom79.mytransl.client.HttpsConnectionException; +import com.github.stom79.mytransl.client.Results; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + + +/** + * Created by @stom79 on 28/11/2017. + * The class which manages the replies + * Changed 10/01/2021 + */ + +@SuppressWarnings({"unused", "RedundantSuppression"}) +public class Translate { + + private static final Pattern hashtagPattern = Pattern.compile("(#[\\w_À-ú-]+)"); + private static final Pattern mentionPattern = Pattern.compile("(@[\\w]+)"); + private static final Pattern mentionOtherInstancePattern = Pattern.compile("(@[\\w]*@[\\w.-]+)"); + private final Translate translate; + private String targetedLanguage; + private String initialLanguage; + private MyTransL.translatorEngine translatorEngine; + private String initialContent; + private String obfuscateContent; + private String translatedContent; + private Params.fType format; + private HashMap tagConversion, mentionConversion, urlConversion, mailConversion; + + public Translate() { + this.translate = this; + } + + private static String replacer(StringBuffer outBuffer) throws UnsupportedEncodingException { + String data = outBuffer.toString(); + data = data.replaceAll("%(?![0-9a-fA-F]{2})", "%25"); + data = data.replaceAll("\\+", "%2B"); + data = URLDecoder.decode(data, "utf-8"); + return data; + } + + public Params.fType getFormat() { + return format; + } + + public void setFormat(Params.fType format) { + this.format = format; + } + + public String getTargetedLanguage() { + return targetedLanguage; + } + + public void setTargetedLanguage(String targetedLanguage) { + this.targetedLanguage = targetedLanguage; + } + + public String getInitialLanguage() { + return initialLanguage; + } + + private void setInitialLanguage(String initialLanguage) { + this.initialLanguage = initialLanguage; + } + + public String getInitialContent() { + return initialContent; + } + + public void setInitialContent(String initialContent) { + this.initialContent = initialContent; + } + + public String getTranslatedContent() { + return translatedContent; + } + + private void setTranslatedContent(String translatedContent) { + this.translatedContent = translatedContent; + } + + public MyTransL.translatorEngine getTranslatorEngine() { + return translatorEngine; + } + + public void setTranslatorEngine(MyTransL.translatorEngine translatorEngine) { + this.translatorEngine = translatorEngine; + } + + public HashMap getTagConversion() { + return this.tagConversion; + } + + public HashMap getMentionConversion() { + return this.mentionConversion; + } + + public HashMap getUrlConversion() { + return this.urlConversion; + } + + public HashMap getMailConversion() { + return this.mailConversion; + } + + public String getObfuscateContent() { + return this.obfuscateContent; + } + + public void obfuscate() { + + this.tagConversion = new HashMap<>(); + this.mentionConversion = new HashMap<>(); + this.urlConversion = new HashMap<>(); + this.mailConversion = new HashMap<>(); + SpannableString spannableString; + String content = this.translate.getInitialContent(); + content = content.replaceAll("\n", "
"); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + spannableString = new SpannableString(Html.fromHtml(content, Html.FROM_HTML_MODE_LEGACY)); + } else { + spannableString = new SpannableString(Html.fromHtml(content)); + } + String text = spannableString.toString(); + Matcher matcher; + + //Mentions with instances (@name@domain) will be replaced by __o0__, __o1__, etc. + int i = 0; + matcher = mentionOtherInstancePattern.matcher(text); + while (matcher.find()) { + String key = "$o" + i; + String value = matcher.group(0); + if (value != null) { + this.mentionConversion.put(key, value); + text = text.replace(value, key); + } + i++; + } + //Extracts Emails + matcher = Patterns.EMAIL_ADDRESS.matcher(text); + i = 0; + //replaces them by a kind of variable which shouldn't be translated ie: __e0__, __e1__, etc. + while (matcher.find()) { + String key = "$e" + i; + String value = matcher.group(0); + if (value != null) { + this.mailConversion.put(key, value); + text = text.replace(value, key); + } + i++; + } + + //Same for mentions with __m0__, __m1__, etc. + i = 0; + matcher = mentionPattern.matcher(text); + while (matcher.find()) { + String key = "$m" + i; + String value = matcher.group(0); + if (value != null) { + this.mentionConversion.put(key, value); + text = text.replace(value, key); + } + i++; + } + + //Extracts urls + matcher = Patterns.WEB_URL.matcher(text); + i = 0; + //replaces them by a kind of variable which shouldn't be translated ie: __u0__, __u1__, etc. + while (matcher.find()) { + String key = "$u" + i; + String value = matcher.group(0); + int end = matcher.end(); + if (spannableString.length() > end && spannableString.charAt(end) == '/') { + text = spannableString.toString().substring(0, end). + concat(spannableString.toString().substring(end + 1, spannableString.length())); + } + if (value != null) { + this.urlConversion.put(key, value); + text = text.replace(value, key); + } + i++; + } + i = 0; + //Same for tags with __t0__, __t1__, etc. + matcher = hashtagPattern.matcher(text); + while (matcher.find()) { + String key = "$t" + i; + String value = matcher.group(0); + if (value != null) { + this.tagConversion.put(key, value); + text = text.replace(value, key); + } + i++; + } + this.obfuscateContent = text; + } + + public void deobfuscate() { + String aJsonString = null; + try { + if (translatorEngine == MyTransL.translatorEngine.YANDEX) + aJsonString = yandexTranslateToText(translatedContent); + else + aJsonString = translatedContent; + if (aJsonString != null) { + if (this.urlConversion != null) { + Iterator> itU = this.urlConversion.entrySet().iterator(); + while (itU.hasNext()) { + Map.Entry pair = itU.next(); + aJsonString = aJsonString.replace(pair.getKey(), pair.getValue()); + itU.remove(); + } + } + if (this.tagConversion != null) { + Iterator> itT = this.tagConversion.entrySet().iterator(); + while (itT.hasNext()) { + Map.Entry pair = itT.next(); + aJsonString = aJsonString.replace(pair.getKey(), pair.getValue()); + itT.remove(); + } + } + if (this.mentionConversion != null) { + Iterator> itM = this.mentionConversion.entrySet().iterator(); + while (itM.hasNext()) { + Map.Entry pair = itM.next(); + aJsonString = aJsonString.replace(pair.getKey(), pair.getValue()); + itM.remove(); + } + } + if (this.mailConversion != null) { + Iterator> itE = this.mailConversion.entrySet().iterator(); + while (itE.hasNext()) { + Map.Entry pair = itE.next(); + aJsonString = aJsonString.replace(pair.getKey(), pair.getValue()); + itE.remove(); + } + } + } + } catch (UnsupportedEncodingException | IllegalArgumentException e) { + e.printStackTrace(); + } + if (aJsonString != null) + translatedContent = aJsonString; + } + + private String yandexTranslateToText(String text) throws UnsupportedEncodingException { + if (text == null) + return null; + /* The one instance where I've seen this happen, + the special tag was originally a hashtag ("__t1__"), + that Yandex decided to change to a "__q1 - __". + */ + text = text.replaceAll("__q(\\d+) - __", "\\$t$1"); + // Noticed this in the very same toot + text = text.replace("&", "&"); + text = replacer(new StringBuffer(text)); + return text; + } + + /*** + * Method to parse result coming from the Yandex translator + * More about Yandex translate API - https://tech.yandex.com/translate/ + * @param response String - Response of the engine translator + * @param listener - Results Listener + */ + public void parseYandexResult(String response, Results listener) { + translate.setTranslatorEngine(MyTransL.translatorEngine.YANDEX); + try { + JSONObject translationJson = new JSONObject(response); + //Retrieves the translated content + JSONArray aJsonArray = translationJson.getJSONArray("text"); + String aJsonString = aJsonArray.get(0).toString(); + aJsonString = aJsonString.replace("&", "&"); + aJsonString = replacer(new StringBuffer(aJsonString)); + translate.setTranslatedContent(aJsonString); + //Retrieves the translation direction + String translationDirection = translationJson.get("lang").toString(); + String[] td = translationDirection.split("-"); + translate.setInitialLanguage(td[0]); + translate.setTargetedLanguage(td[1]); + } catch (JSONException | UnsupportedEncodingException e1) { + HttpsConnectionException httpsConnectionException = new HttpsConnectionException(-1, e1.getMessage()); + listener.onFail(httpsConnectionException); + } + } + + /*** + * Method to parse result coming from the Deepl translator + * More about Deepl translate API - https://www.deepl.com/api-reference.html + * @param response String - Response of the engine translator + * @param listener - Results Listener + */ + public void parseLibreTranslateResult(String response, Results listener) { + translate.setTranslatorEngine(MyTransL.translatorEngine.DEEPL); + try { + JSONObject translationJson = new JSONObject(response); + //Retrieves the translated content + translate.setTranslatedContent(translationJson.getString("translatedText")); + //Retrieves the initial language + translate.setInitialLanguage(initialLanguage); + } catch (JSONException e1) { + e1.printStackTrace(); + HttpsConnectionException httpsConnectionException = new HttpsConnectionException(-1, e1.getMessage()); + listener.onFail(httpsConnectionException); + } + } + + + /*** + * Method to parse result coming from the Deepl translator + * More about Deepl translate API - https://www.deepl.com/api-reference.html + * @param response String - Response of the engine translator + * @param listener - Results Listener + */ + public void parseDeeplResult(String response, Results listener) { + translate.setTranslatorEngine(MyTransL.translatorEngine.DEEPL); + try { + JSONObject translationJson = new JSONObject(response); + //Retrieves the translated content + JSONArray aJsonArray = translationJson.getJSONArray("translations"); + JSONObject aJsonString = aJsonArray.getJSONObject(0); + translate.setTranslatedContent(aJsonString.getString("text")); + //Retrieves the initial language + translate.setInitialLanguage(initialLanguage); + } catch (JSONException e1) { + e1.printStackTrace(); + HttpsConnectionException httpsConnectionException = new HttpsConnectionException(-1, e1.getMessage()); + listener.onFail(httpsConnectionException); + } + } + + + /*** + * Method to parse result coming from the Systrans translator + * More about Systran translate API - https://platform.systran.net/reference/translation + * @param response String - Response of the engine translator + * @param listener - Results Listener + */ + public void parseSystranlResult(String response, Results listener) { + translate.setTranslatorEngine(MyTransL.translatorEngine.SYSTRAN); + try { + JSONObject translationJson = new JSONObject(response); + //Retrieves the translated content + JSONArray aJsonArray = translationJson.getJSONArray("outputs"); + JSONObject aJsonString = aJsonArray.getJSONObject(0); + translate.setTranslatedContent(aJsonString.getString("output")); + //Retrieves the initial language + translate.setInitialLanguage(initialLanguage); + } catch (JSONException e1) { + e1.printStackTrace(); + HttpsConnectionException httpsConnectionException = new HttpsConnectionException(-1, e1.getMessage()); + listener.onFail(httpsConnectionException); + } + } +} diff --git a/ratethisapp/.gitignore b/ratethisapp/.gitignore new file mode 100644 index 000000000..4380595bb --- /dev/null +++ b/ratethisapp/.gitignore @@ -0,0 +1,23 @@ +# built application files +*.apk +*.ap_ + +# files for the dex VM +*.dex + +# Java class files +*.class + +# generated files +bin/ +gen/ +build/ + +# Local configuration file (sdk path, etc) +local.properties + +# Eclipse project files +.classpath +.project + +.DS_Store diff --git a/ratethisapp/build.gradle b/ratethisapp/build.gradle new file mode 100644 index 000000000..f4cd3f1a8 --- /dev/null +++ b/ratethisapp/build.gradle @@ -0,0 +1,22 @@ +apply plugin: "com.android.library" + +android { + compileSdkVersion 31 + + defaultConfig { + minSdkVersion 21 + targetSdkVersion 31 + versionCode 1 + versionName "1.0" + } + + buildTypes { + release { + minifyEnabled false + } + } +} + +dependencies { + implementation 'androidx.annotation:annotation:1.3.0' +} diff --git a/ratethisapp/gradle.properties b/ratethisapp/gradle.properties new file mode 100644 index 000000000..f12455b76 --- /dev/null +++ b/ratethisapp/gradle.properties @@ -0,0 +1,3 @@ +POM_NAME=Android-RateThisApp Library +POM_ARTIFACT_ID=ratethisapp +POM_PACKAGING=aar diff --git a/ratethisapp/proguard-rules.pro b/ratethisapp/proguard-rules.pro new file mode 100644 index 000000000..5b86c0858 --- /dev/null +++ b/ratethisapp/proguard-rules.pro @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in ${sdk.dir}/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/ratethisapp/src/main/AndroidManifest.xml b/ratethisapp/src/main/AndroidManifest.xml new file mode 100644 index 000000000..380ea47b7 --- /dev/null +++ b/ratethisapp/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + + diff --git a/ratethisapp/src/main/java/com/kobakei/ratethisapp/RateThisApp.java b/ratethisapp/src/main/java/com/kobakei/ratethisapp/RateThisApp.java new file mode 100644 index 000000000..2398363de --- /dev/null +++ b/ratethisapp/src/main/java/com/kobakei/ratethisapp/RateThisApp.java @@ -0,0 +1,516 @@ +/* + * Copyright 2013-2017 Keisuke Kobayashi + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.kobakei.ratethisapp; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnCancelListener; +import android.content.DialogInterface.OnClickListener; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.SharedPreferences.Editor; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.Build; +import android.text.TextUtils; +import android.util.Log; +import android.view.KeyEvent; + +import androidx.annotation.StringRes; + +import java.lang.ref.WeakReference; +import java.util.Date; +import java.util.concurrent.TimeUnit; + +/** + * RateThisApp
+ * A library to show the app rate dialog + * + * @author Keisuke Kobayashi (k.kobayashi.122@gmail.com) + */ +public class RateThisApp { + + /** + * If true, print LogCat + */ + public static final boolean DEBUG = false; + private static final String TAG = RateThisApp.class.getSimpleName(); + private static final String PREF_NAME = "RateThisApp"; + private static final String KEY_INSTALL_DATE = "rta_install_date"; + private static final String KEY_LAUNCH_TIMES = "rta_launch_times"; + private static final String KEY_OPT_OUT = "rta_opt_out"; + private static final String KEY_ASK_LATER_DATE = "rta_ask_later_date"; + private static Date mInstallDate = new Date(); + private static int mLaunchTimes = 0; + private static boolean mOptOut = false; + private static Date mAskLaterDate = new Date(); + private static Config sConfig = new Config(); + private static Callback sCallback = null; + // Weak ref to avoid leaking the context + private static WeakReference sDialogRef = null; + + /** + * Initialize RateThisApp configuration. + * + * @param config Configuration object. + */ + public static void init(Config config) { + sConfig = config; + } + + /** + * Set callback instance. + * The callback will receive yes/no/later events. + * + * @param callback + */ + public static void setCallback(Callback callback) { + sCallback = callback; + } + + /** + * Call this API when the launcher activity is launched.
+ * It is better to call this API in onCreate() of the launcher activity. + * + * @param context Context + */ + public static void onCreate(Context context) { + SharedPreferences pref = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); + Editor editor = pref.edit(); + // If it is the first launch, save the date in shared preference. + if (pref.getLong(KEY_INSTALL_DATE, 0) == 0L) { + storeInstallDate(context, editor); + } + // Increment launch times + int launchTimes = pref.getInt(KEY_LAUNCH_TIMES, 0); + launchTimes++; + editor.putInt(KEY_LAUNCH_TIMES, launchTimes); + log("Launch times; " + launchTimes); + + editor.apply(); + + mInstallDate = new Date(pref.getLong(KEY_INSTALL_DATE, 0)); + mLaunchTimes = pref.getInt(KEY_LAUNCH_TIMES, 0); + mOptOut = pref.getBoolean(KEY_OPT_OUT, false); + mAskLaterDate = new Date(pref.getLong(KEY_ASK_LATER_DATE, 0)); + + printStatus(context); + } + + /** + * This API is deprecated. + * You should call onCreate instead of this API in Activity's onCreate(). + * + * @param context + */ + @Deprecated + public static void onStart(Context context) { + onCreate(context); + } + + /** + * Show the rate dialog if the criteria is satisfied. + * + * @param context Context + * @return true if shown, false otherwise. + */ + public static boolean showRateDialogIfNeeded(final Context context) { + if (shouldShowRateDialog()) { + showRateDialog(context); + return true; + } else { + return false; + } + } + + /** + * Show the rate dialog if the criteria is satisfied. + * + * @param context Context + * @param themeId Theme ID + * @return true if shown, false otherwise. + */ + public static boolean showRateDialogIfNeeded(final Context context, int themeId) { + if (shouldShowRateDialog()) { + showRateDialog(context, themeId); + return true; + } else { + return false; + } + } + + /** + * Check whether the rate dialog should be shown or not. + * Developers may call this method directly if they want to show their own view instead of + * dialog provided by this library. + * + * @return + */ + public static boolean shouldShowRateDialog() { + if (mOptOut) { + return false; + } else { + if (mLaunchTimes >= sConfig.mCriteriaLaunchTimes) { + return true; + } + long threshold = TimeUnit.DAYS.toMillis(sConfig.mCriteriaInstallDays); // msec + return new Date().getTime() - mInstallDate.getTime() >= threshold && + new Date().getTime() - mAskLaterDate.getTime() >= threshold; + } + } + + /** + * Show the rate dialog + * + * @param context + */ + public static void showRateDialog(final Context context) { + AlertDialog.Builder builder = new AlertDialog.Builder(context); + showRateDialog(context, builder); + } + + /** + * Show the rate dialog + * + * @param context + * @param themeId + */ + public static void showRateDialog(final Context context, int themeId) { + AlertDialog.Builder builder = new AlertDialog.Builder(context, themeId); + showRateDialog(context, builder); + } + + /** + * Stop showing the rate dialog + * + * @param context + */ + public static void stopRateDialog(final Context context) { + setOptOut(context, true); + } + + /** + * Get count number of the rate dialog launches + * + * @return + */ + public static int getLaunchCount(final Context context) { + SharedPreferences pref = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); + return pref.getInt(KEY_LAUNCH_TIMES, 0); + } + + private static void showRateDialog(final Context context, AlertDialog.Builder builder) { + if (sDialogRef != null && sDialogRef.get() != null) { + // Dialog is already present + return; + } + + int titleId = sConfig.mTitleId != 0 ? sConfig.mTitleId : R.string.rta_dialog_title; + int messageId = sConfig.mMessageId != 0 ? sConfig.mMessageId : R.string.rta_dialog_message; + int cancelButtonID = sConfig.mCancelButton != 0 ? sConfig.mCancelButton : R.string.rta_dialog_cancel; + int thanksButtonID = sConfig.mNoButtonId != 0 ? sConfig.mNoButtonId : R.string.rta_dialog_no; + int rateButtonID = sConfig.mYesButtonId != 0 ? sConfig.mYesButtonId : R.string.rta_dialog_ok; + builder.setTitle(titleId); + builder.setMessage(messageId); + switch (sConfig.mCancelMode) { + case Config.CANCEL_MODE_BACK_KEY_OR_TOUCH_OUTSIDE: + builder.setCancelable(true); // It's the default anyway + break; + case Config.CANCEL_MODE_BACK_KEY: + builder.setCancelable(false); + builder.setOnKeyListener(new DialogInterface.OnKeyListener() { + @Override + public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_BACK) { + dialog.cancel(); + return true; + } else { + return false; + } + } + }); + break; + case Config.CANCEL_MODE_NONE: + builder.setCancelable(false); + break; + } + builder.setPositiveButton(rateButtonID, new OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + if (sCallback != null) { + sCallback.onYesClicked(); + } + String appPackage = context.getPackageName(); + String url = "market://details?id=" + appPackage; + if (!TextUtils.isEmpty(sConfig.mUrl)) { + url = sConfig.mUrl; + } + try { + context.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url))); + } catch (android.content.ActivityNotFoundException anfe) { + context.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("http://play.google.com/store/apps/details?id=" + context.getPackageName()))); + } + setOptOut(context, true); + } + }); + builder.setNeutralButton(cancelButtonID, new OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + if (sCallback != null) { + sCallback.onCancelClicked(); + } + clearSharedPreferences(context); + storeAskLaterDate(context); + } + }); + builder.setNegativeButton(thanksButtonID, new OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + if (sCallback != null) { + sCallback.onNoClicked(); + } + setOptOut(context, true); + } + }); + builder.setOnCancelListener(new OnCancelListener() { + @Override + public void onCancel(DialogInterface dialog) { + if (sCallback != null) { + sCallback.onCancelClicked(); + } + clearSharedPreferences(context); + storeAskLaterDate(context); + } + }); + builder.setOnDismissListener(new DialogInterface.OnDismissListener() { + @Override + public void onDismiss(DialogInterface dialog) { + sDialogRef.clear(); + } + }); + sDialogRef = new WeakReference<>(builder.show()); + } + + /** + * Clear data in shared preferences.
+ * This API is called when the "Later" is pressed or canceled. + * + * @param context + */ + private static void clearSharedPreferences(Context context) { + SharedPreferences pref = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); + Editor editor = pref.edit(); + editor.remove(KEY_INSTALL_DATE); + editor.remove(KEY_LAUNCH_TIMES); + editor.apply(); + } + + /** + * Set opt out flag. + * If it is true, the rate dialog will never shown unless app data is cleared. + * This method is called when Yes or No is pressed. + * + * @param context + * @param optOut + */ + private static void setOptOut(final Context context, boolean optOut) { + SharedPreferences pref = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); + Editor editor = pref.edit(); + editor.putBoolean(KEY_OPT_OUT, optOut); + editor.apply(); + mOptOut = optOut; + } + + /** + * Store install date. + * Install date is retrieved from package manager if possible. + * + * @param context + * @param editor + */ + private static void storeInstallDate(final Context context, SharedPreferences.Editor editor) { + Date installDate = new Date(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) { + PackageManager packMan = context.getPackageManager(); + try { + PackageInfo pkgInfo = packMan.getPackageInfo(context.getPackageName(), 0); + installDate = new Date(pkgInfo.firstInstallTime); + } catch (PackageManager.NameNotFoundException e) { + e.printStackTrace(); + } + } + editor.putLong(KEY_INSTALL_DATE, installDate.getTime()); + log("First install: " + installDate); + } + + /** + * Store the date the user asked for being asked again later. + * + * @param context + */ + private static void storeAskLaterDate(final Context context) { + SharedPreferences pref = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); + Editor editor = pref.edit(); + editor.putLong(KEY_ASK_LATER_DATE, System.currentTimeMillis()); + editor.apply(); + } + + /** + * Print values in SharedPreferences (used for debug) + * + * @param context + */ + private static void printStatus(final Context context) { + SharedPreferences pref = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); + log("*** RateThisApp Status ***"); + log("Install Date: " + new Date(pref.getLong(KEY_INSTALL_DATE, 0))); + log("Launch Times: " + pref.getInt(KEY_LAUNCH_TIMES, 0)); + log("Opt out: " + pref.getBoolean(KEY_OPT_OUT, false)); + } + + /** + * Print log if enabled + * + * @param message + */ + private static void log(String message) { + if (DEBUG) { + Log.v(TAG, message); + } + } + + /** + * Callback of dialog click event + */ + public interface Callback { + /** + * "Rate now" event + */ + void onYesClicked(); + + /** + * "No, thanks" event + */ + void onNoClicked(); + + /** + * "Later" event + */ + void onCancelClicked(); + } + + /** + * RateThisApp configuration. + */ + public static class Config { + public static final int CANCEL_MODE_BACK_KEY_OR_TOUCH_OUTSIDE = 0; + public static final int CANCEL_MODE_BACK_KEY = 1; + public static final int CANCEL_MODE_NONE = 2; + private final int mCriteriaInstallDays; + private final int mCriteriaLaunchTimes; + private String mUrl = null; + private int mTitleId = 0; + private int mMessageId = 0; + private int mYesButtonId = 0; + private int mNoButtonId = 0; + private int mCancelButton = 0; + private int mCancelMode = CANCEL_MODE_BACK_KEY_OR_TOUCH_OUTSIDE; + + /** + * Constructor with default criteria. + */ + public Config() { + this(7, 10); + } + + /** + * Constructor. + * + * @param criteriaInstallDays + * @param criteriaLaunchTimes + */ + public Config(int criteriaInstallDays, int criteriaLaunchTimes) { + this.mCriteriaInstallDays = criteriaInstallDays; + this.mCriteriaLaunchTimes = criteriaLaunchTimes; + } + + /** + * Set title string ID. + * + * @param stringId + */ + public void setTitle(@StringRes int stringId) { + this.mTitleId = stringId; + } + + /** + * Set message string ID. + * + * @param stringId + */ + public void setMessage(@StringRes int stringId) { + this.mMessageId = stringId; + } + + /** + * Set rate now string ID. + * + * @param stringId + */ + public void setYesButtonText(@StringRes int stringId) { + this.mYesButtonId = stringId; + } + + /** + * Set no thanks string ID. + * + * @param stringId + */ + public void setNoButtonText(@StringRes int stringId) { + this.mNoButtonId = stringId; + } + + /** + * Set cancel string ID. + * + * @param stringId + */ + public void setCancelButtonText(@StringRes int stringId) { + this.mCancelButton = stringId; + } + + /** + * Set navigation url when user clicks rate button. + * Typically, url will be https://play.google.com/store/apps/details?id=PACKAGE_NAME for Google Play. + * + * @param url + */ + public void setUrl(String url) { + this.mUrl = url; + } + + /** + * Set the cancel mode; namely, which ways the user can cancel the dialog. + * + * @param cancelMode + */ + public void setCancelMode(int cancelMode) { + this.mCancelMode = cancelMode; + } + } +} diff --git a/ratethisapp/src/main/res/value-vi/string.xml b/ratethisapp/src/main/res/value-vi/string.xml new file mode 100644 index 000000000..9303d87fa --- /dev/null +++ b/ratethisapp/src/main/res/value-vi/string.xml @@ -0,0 +1,11 @@ + + + Đánh giá ứng dụng này + Nếu bạn thích ứng dụng này, bạn có phiền nếu dành một chút + thời gian để đánh giá ứng dụng? Nó sẽ không mất thời gian của bạn hơn một phút. Cám ơn sự hỗ + trợ của bạn rất nhiều! + + Rate ngay và luôn + Để sau nha + Không hiện lại nữa + diff --git a/ratethisapp/src/main/res/values-ar/strings.xml b/ratethisapp/src/main/res/values-ar/strings.xml new file mode 100644 index 000000000..1c0fb4fbd --- /dev/null +++ b/ratethisapp/src/main/res/values-ar/strings.xml @@ -0,0 +1,9 @@ + + + قيم هذا التطبيق + إذا احببت هذا التطبيق أبدي ملاحظاتك ، افكارك من خلال تقييمه على سوق بلاي + تقييم + لاحقا + لا شكرا + + diff --git a/ratethisapp/src/main/res/values-az/strings.xml b/ratethisapp/src/main/res/values-az/strings.xml new file mode 100644 index 000000000..d2599de70 --- /dev/null +++ b/ratethisapp/src/main/res/values-az/strings.xml @@ -0,0 +1,9 @@ + + + Bu tətbiqi dəyərləndirin + Əgər tətbiqdən razı qaldınızsa, zəhmət olmasa bir neçə saniyənizi ayırıb tətbiqi dəyərləndirin. Dəstəyiniz üçün təşəkkür edirik! + Qiymətləndir + Sonra xatırlat + İmtina et + + diff --git a/ratethisapp/src/main/res/values-bg/strings.xml b/ratethisapp/src/main/res/values-bg/strings.xml new file mode 100644 index 000000000..3a1021e9d --- /dev/null +++ b/ratethisapp/src/main/res/values-bg/strings.xml @@ -0,0 +1,9 @@ + + + Оцени приложението + Ако харесвате приложението, моля отделете малко време, за да го оцените. Няма да отнеме повече от минута. Благодаря за подкрепата! + Оцени сега + По-късно + Не, благодаря + + diff --git a/ratethisapp/src/main/res/values-cs/strings.xml b/ratethisapp/src/main/res/values-cs/strings.xml new file mode 100644 index 000000000..d972161a6 --- /dev/null +++ b/ratethisapp/src/main/res/values-cs/strings.xml @@ -0,0 +1,9 @@ + + + Ohodnotit aplikaci + Pokud se Vám aplikace líbí, rádi bychom Vás poprosili o její ohodnocení. Nezabere to více než minutu. Děkujeme za Vaši podporu! + Ohodnotit nyní + Připomenout později + Ne, díky + + diff --git a/ratethisapp/src/main/res/values-da/strings.xml b/ratethisapp/src/main/res/values-da/strings.xml new file mode 100644 index 000000000..ddc3b3e8d --- /dev/null +++ b/ratethisapp/src/main/res/values-da/strings.xml @@ -0,0 +1,9 @@ + + + Bedøm appen + Hvis du synes om appen, må du gerne bedømme den. Det tager højst et minut. Tak for din hjælp. + Bedøm nu + Senere + Nej tak + + diff --git a/ratethisapp/src/main/res/values-de/strings.xml b/ratethisapp/src/main/res/values-de/strings.xml new file mode 100644 index 000000000..c12ecb627 --- /dev/null +++ b/ratethisapp/src/main/res/values-de/strings.xml @@ -0,0 +1,9 @@ + + + Bewerte diese App + Wenn dir diese App gefällt, nimm dir doch einen Moment Zeit und bewerte sie. Danke für deine Unterstützung! + Jetzt bewerten + Später + Nein, Danke + + diff --git a/ratethisapp/src/main/res/values-es/strings.xml b/ratethisapp/src/main/res/values-es/strings.xml new file mode 100644 index 000000000..d4fe37801 --- /dev/null +++ b/ratethisapp/src/main/res/values-es/strings.xml @@ -0,0 +1,9 @@ + + + Evalúa esta aplicación + Si te gusta esta aplicación, toma un momento para evaluarla. No tomará más de un minuto. ¡Gracias por tu apoyo! + Evaluar + Más tarde + No, gracias + + diff --git a/ratethisapp/src/main/res/values-eu/strings.xml b/ratethisapp/src/main/res/values-eu/strings.xml new file mode 100644 index 000000000..e7e7a57e7 --- /dev/null +++ b/ratethisapp/src/main/res/values-eu/strings.xml @@ -0,0 +1,9 @@ + + + Aplikazioa baloratu + Aplikazioa gustatzen bazaizu, aipamenen bat idatz zenezake? ez duzu minutu bat baino gehiago beharko. Eskerrik asko zure babesagatik! + Baloratu + Beranduago + Ez, eskerrik asko + + diff --git a/ratethisapp/src/main/res/values-fi/strings.xml b/ratethisapp/src/main/res/values-fi/strings.xml new file mode 100644 index 000000000..6f1a4d76b --- /dev/null +++ b/ratethisapp/src/main/res/values-fi/strings.xml @@ -0,0 +1,9 @@ + + + Arvostele sovellus + Jos tykkäät sovelluksesta, ole hyvä ja arvostelese? Kiitos tuestasi! + Arvostele + Myöhemmin + Ei kiitos + + diff --git a/ratethisapp/src/main/res/values-fr/strings.xml b/ratethisapp/src/main/res/values-fr/strings.xml new file mode 100644 index 000000000..668fb423b --- /dev/null +++ b/ratethisapp/src/main/res/values-fr/strings.xml @@ -0,0 +1,9 @@ + + + Noter cette application + Si vous appréciez cette application, veuillez prendre un moment pour la noter. Cela ne prendra qu\'une minute. Merci de votre participation ! + Maintenant + Plus tard + Non, merci + + diff --git a/ratethisapp/src/main/res/values-gr/strings.xml b/ratethisapp/src/main/res/values-gr/strings.xml new file mode 100644 index 000000000..2a4090c4d --- /dev/null +++ b/ratethisapp/src/main/res/values-gr/strings.xml @@ -0,0 +1,9 @@ + + + + ; . ! + + + , + + diff --git a/ratethisapp/src/main/res/values-hr/strings.xml b/ratethisapp/src/main/res/values-hr/strings.xml new file mode 100644 index 000000000..e7d74a994 --- /dev/null +++ b/ratethisapp/src/main/res/values-hr/strings.xml @@ -0,0 +1,8 @@ + + + Ocijeni aplikaciju + Ako Vam se sviđa ova aplikacija, molimo Vas da odvojite trenutak kako bi je ocijenili. Neće trajati više od minute. Hvala! + Ocijeni + Podjeti me kasnije + Ne, hvala + diff --git a/ratethisapp/src/main/res/values-hu/strings.xml b/ratethisapp/src/main/res/values-hu/strings.xml new file mode 100644 index 000000000..8955a1801 --- /dev/null +++ b/ratethisapp/src/main/res/values-hu/strings.xml @@ -0,0 +1,9 @@ + + + Értékeld az alkalmazást + Ha tetszik az alkalmazás kérlek értékeld. Csak egy percet vesz igénybe. Köszönöm a segítségedet! + Értékelem + Később + Nem most + + diff --git a/ratethisapp/src/main/res/values-it/strings.xml b/ratethisapp/src/main/res/values-it/strings.xml new file mode 100644 index 000000000..0cc9e73c0 --- /dev/null +++ b/ratethisapp/src/main/res/values-it/strings.xml @@ -0,0 +1,9 @@ + + + Vota quest\'app + Se ti piace usare quest\'app, ti dispiacerebbe trovare un momento per votarla? Ti richiederà meno di un minuto. Grazie per il tuo supporto! + Vota + Più tardi + No, grazie + + diff --git a/ratethisapp/src/main/res/values-ja/strings.xml b/ratethisapp/src/main/res/values-ja/strings.xml new file mode 100644 index 000000000..522f4f2f6 --- /dev/null +++ b/ratethisapp/src/main/res/values-ja/strings.xml @@ -0,0 +1,9 @@ + + + アプリをレビューしませんか? + このアプリを気に入っていただいたら、ぜひレビューしてください。レビューは1分未満で終わります。 + はい + あとで + いいえ + + diff --git a/ratethisapp/src/main/res/values-ko/strings.xml b/ratethisapp/src/main/res/values-ko/strings.xml new file mode 100644 index 000000000..caf4c8421 --- /dev/null +++ b/ratethisapp/src/main/res/values-ko/strings.xml @@ -0,0 +1,9 @@ + + + 앱 평가하기 + 이 앱이 만족스럽다면 평가를 부탁드립니다. 일분도 걸리지 않아요. 지원에 감사합니다. + 지금 평가하기 + 나중에 할게요 + 안 할래요 + + diff --git a/ratethisapp/src/main/res/values-nl/strings.xml b/ratethisapp/src/main/res/values-nl/strings.xml new file mode 100644 index 000000000..317ea5e19 --- /dev/null +++ b/ratethisapp/src/main/res/values-nl/strings.xml @@ -0,0 +1,9 @@ + + + Beoordeel de app + We vinden uw mening belangrijk. Als u tevreden bent over onze app dan stellen wij het op prijs als u dat met andere gebruikers wilt delen. Het beoordelen kost minder dan 1 minuut. Bedankt voor uw ondersteuning! + Beoordeel Nu + Later + Nee, Bedankt + + diff --git a/ratethisapp/src/main/res/values-pl/strings.xml b/ratethisapp/src/main/res/values-pl/strings.xml new file mode 100644 index 000000000..9d7de0527 --- /dev/null +++ b/ratethisapp/src/main/res/values-pl/strings.xml @@ -0,0 +1,9 @@ + + + Oceń aplikację + Jeżeli spodobała Ci się ta aplikacja, poświęć chwilę by ją ocenić. To nie zajmie dłużej niż jedną minutę. Z góry dziękuję za Twoje wsparcie! + Oceń teraz + Przypomnij później + Nie dziękuję + + diff --git a/ratethisapp/src/main/res/values-pt-rBR/strings.xml b/ratethisapp/src/main/res/values-pt-rBR/strings.xml new file mode 100644 index 000000000..319f6d8ba --- /dev/null +++ b/ratethisapp/src/main/res/values-pt-rBR/strings.xml @@ -0,0 +1,12 @@ + + + Qualifique esse Aplicativo + + Se você gosta de usar esse aplicativo, se importaria de qualificá-lo? Não irá tomar mais de um minuto. + Obrigado pelo o seu apoio. + + Qualificar + Depois + Não, obrigado + + diff --git a/ratethisapp/src/main/res/values-pt/strings.xml b/ratethisapp/src/main/res/values-pt/strings.xml new file mode 100644 index 000000000..319f6d8ba --- /dev/null +++ b/ratethisapp/src/main/res/values-pt/strings.xml @@ -0,0 +1,12 @@ + + + Qualifique esse Aplicativo + + Se você gosta de usar esse aplicativo, se importaria de qualificá-lo? Não irá tomar mais de um minuto. + Obrigado pelo o seu apoio. + + Qualificar + Depois + Não, obrigado + + diff --git a/ratethisapp/src/main/res/values-ru/strings.xml b/ratethisapp/src/main/res/values-ru/strings.xml new file mode 100644 index 000000000..4f249cf00 --- /dev/null +++ b/ratethisapp/src/main/res/values-ru/strings.xml @@ -0,0 +1,9 @@ + + + Оцените приложение + Если вам понравилось это приложение, пожалуйста, найдите время, чтобы оценить его. Это не займет больше минуты. Спасибо за вашу поддержку! + Оценить сейчас + Напомнить позже + Нет, спасибо + + diff --git a/ratethisapp/src/main/res/values-sk/strings.xml b/ratethisapp/src/main/res/values-sk/strings.xml new file mode 100644 index 000000000..0fcdfbf7f --- /dev/null +++ b/ratethisapp/src/main/res/values-sk/strings.xml @@ -0,0 +1,9 @@ + + + Ohodnotiť aplikáciu + Pokiaľ sa Vám aplikacia páči, radi by sme Vás poprosili o jej ohodnotenie. Nezaberie to viac než minútu. Ďakujeme za Vašu podporu! + Ohodnotiť teraz + Pripomenúť neskôr + Nie, ďakujem + + diff --git a/ratethisapp/src/main/res/values-sv/strings.xml b/ratethisapp/src/main/res/values-sv/strings.xml new file mode 100644 index 000000000..952987cbf --- /dev/null +++ b/ratethisapp/src/main/res/values-sv/strings.xml @@ -0,0 +1,9 @@ + + + Betygsätt appen + Om du gillar denna app, sätt gärna betyg på den. Det tar inte mer än ett ögonblick. Tack för ditt stöd! + Betygsätt nu + Senare + Nej, tack + + diff --git a/ratethisapp/src/main/res/values-th/strings.xml b/ratethisapp/src/main/res/values-th/strings.xml new file mode 100644 index 000000000..14993a870 --- /dev/null +++ b/ratethisapp/src/main/res/values-th/strings.xml @@ -0,0 +1,9 @@ + + + ประเมินแอพนี้ + ถ้าคุณพอใจในการใช้งานแอพนี้ ขอเวลาคุณสักครู่ในการประเมินแอพ \nขอบคุณสำหรับการสนับสนุน! + ประเมิน + ไว้ทีหลัง + ไม่, ขอบคุณ + + diff --git a/ratethisapp/src/main/res/values-tr/strings.xml b/ratethisapp/src/main/res/values-tr/strings.xml new file mode 100644 index 000000000..c9cfdc234 --- /dev/null +++ b/ratethisapp/src/main/res/values-tr/strings.xml @@ -0,0 +1,9 @@ + + + Bu uygulamaya puan verin + Eğer uygulamamızdan memnun kaldıysanız, lütfen birkaç saniyenizi ayırarak bize puan verin. Desteğiniz için teşekkürler! + Puan ver + Hatırlat + Hayır, teşekkürler + + diff --git a/ratethisapp/src/main/res/values-uk/strings.xml b/ratethisapp/src/main/res/values-uk/strings.xml new file mode 100644 index 000000000..3ca1c3da2 --- /dev/null +++ b/ratethisapp/src/main/res/values-uk/strings.xml @@ -0,0 +1,9 @@ + + + Оцініть цей додаток + Якщо Вам подобається цей додаток, чи не могли б Ви знайти час, щоб оцінити його? Це займе не більше хвилини. Дякую за Вашу підтримку! + Оцінити зараз + Пізніше + Ні, дякую + + diff --git a/ratethisapp/src/main/res/values-zh-rTW/strings.xml b/ratethisapp/src/main/res/values-zh-rTW/strings.xml new file mode 100644 index 000000000..426a21b43 --- /dev/null +++ b/ratethisapp/src/main/res/values-zh-rTW/strings.xml @@ -0,0 +1,9 @@ + + + 請為這個App評分 + 如果你喜歡這個App,請花不到一分鐘的時間為這個App評分。謝謝支持! + 立即評分 + 之後提醒 + 不用了,謝謝 + + diff --git a/ratethisapp/src/main/res/values-zh/strings.xml b/ratethisapp/src/main/res/values-zh/strings.xml new file mode 100644 index 000000000..76b7c0cd3 --- /dev/null +++ b/ratethisapp/src/main/res/values-zh/strings.xml @@ -0,0 +1,9 @@ + + + 请为这个软件投票 + 如果你喜欢这个软件,请花些时间为这个软件投票。你都不需要用上一分钟,就可以做到。谢谢你们的支持! + 投票! + 之后提醒 + 不用了,谢谢 + + diff --git a/ratethisapp/src/main/res/values/strings.xml b/ratethisapp/src/main/res/values/strings.xml new file mode 100644 index 000000000..261e73857 --- /dev/null +++ b/ratethisapp/src/main/res/values/strings.xml @@ -0,0 +1,9 @@ + + + Rate this app + If you enjoy using this app, would you mind taking a moment to rate it? It won\'t take more than a minute. Thank you for your support! + Rate now + Later + No, thanks + + diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 000000000..eea69bc0f --- /dev/null +++ b/settings.gradle @@ -0,0 +1,5 @@ +rootProject.name = "Fedilab" +include ':app' +include ':autoimageslider' +include ':mytransl' +include ':ratethisapp'