1
0
mirror of https://github.com/TeamNewPipe/NewPipe synced 2024-07-01 07:04:48 +00:00

Compare commits

...

14 Commits

Author SHA1 Message Date
Isira Seneviratne
165c6cba72
Merge ace7923e2f into d479f29e9b 2024-06-23 06:30:58 +00:00
Isira Seneviratne
ace7923e2f Avoid some potential issues with blocking calls in suspend functions 2024-06-23 11:59:21 +05:30
Isira Seneviratne
dac292181d Remove unused imports 2024-06-23 11:21:08 +05:30
Isira Seneviratne
99f22756e1 Added Coil helper method 2024-06-22 18:32:03 +05:30
Isira Seneviratne
72ce9e3c75 Remove duplicate update methods 2024-06-22 18:07:29 +05:30
Isira Seneviratne
4affc948fe Revert Upsert changes 2024-06-22 17:46:39 +05:30
Isira Seneviratne
5887438dde Fix tests 2024-06-22 17:37:37 +05:30
Isira Seneviratne
8ecd6e6b98 Fix issue with background player 2024-06-22 17:37:34 +05:30
Isira Seneviratne
19c56f80d5 Enable RGB-565 for low-end devices 2024-06-22 17:37:31 +05:30
Isira Seneviratne
e6f01a2d19 Clean up Picasso leftovers 2024-06-22 17:37:24 +05:30
Isira Seneviratne
465fc9f2a8 Migrate to Coil from Picasso 2024-06-22 17:35:40 +05:30
Isira Seneviratne
5b445fd877 Fix compilation errors 2024-06-22 17:32:14 +05:30
Isira Seneviratne
b273c4772e Load notification icons using Coil 2024-06-22 17:32:11 +05:30
Isira Seneviratne
db7a90fad0 Rename .java to .kt 2024-06-22 17:32:08 +05:30
112 changed files with 654 additions and 949 deletions

View File

@ -113,7 +113,7 @@ android {
ext {
checkstyleVersion = '10.12.1'
androidxLifecycleVersion = '2.6.2'
androidxLifecycleVersion = '2.8.2'
androidxRoomVersion = '2.6.1'
androidxWorkVersion = '2.8.1'
@ -222,12 +222,14 @@ dependencies {
implementation 'androidx.fragment:fragment-ktx:1.6.2'
implementation "androidx.lifecycle:lifecycle-livedata-ktx:${androidxLifecycleVersion}"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${androidxLifecycleVersion}"
implementation "androidx.lifecycle:lifecycle-service:${androidxLifecycleVersion}"
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0'
implementation 'androidx.media:media:1.7.0'
implementation 'androidx.preference:preference:1.2.1'
implementation 'androidx.recyclerview:recyclerview:1.3.2'
implementation "androidx.room:room-runtime:${androidxRoomVersion}"
implementation "androidx.room:room-rxjava3:${androidxRoomVersion}"
implementation "androidx.room:room-ktx:${androidxRoomVersion}"
kapt "androidx.room:room-compiler:${androidxRoomVersion}"
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
// Newer version specified to prevent accessibility regressions with RecyclerView, see:
@ -267,8 +269,7 @@ dependencies {
implementation "com.github.lisawray.groupie:groupie-viewbinding:${groupieVersion}"
// Image loading
//noinspection GradleDependency --> 2.8 is the last version, not 2.71828!
implementation "com.squareup.picasso:picasso:2.8"
implementation 'io.coil-kt:coil:2.6.0'
// Markdown library for Android
implementation "io.noties.markwon:core:${markwonVersion}"

View File

@ -129,7 +129,7 @@ class DatabaseMigrationTest {
)
val migratedDatabaseV3 = getMigratedDatabase()
val listFromDB = migratedDatabaseV3.streamDAO().all.blockingFirst()
val listFromDB = migratedDatabaseV3.streamDAO().getAll().blockingFirst()
// Only expect 2, the one with the null url will be ignored
assertEquals(2, listFromDB.size)
@ -217,7 +217,7 @@ class DatabaseMigrationTest {
)
val migratedDatabaseV8 = getMigratedDatabase()
val listFromDB = migratedDatabaseV8.searchHistoryDAO().all.blockingFirst()
val listFromDB = migratedDatabaseV8.searchHistoryDAO().getAll().blockingFirst()
assertEquals(2, listFromDB.size)
assertEquals("abc", listFromDB[0].search)
@ -283,8 +283,8 @@ class DatabaseMigrationTest {
)
val migratedDatabaseV9 = getMigratedDatabase()
var localListFromDB = migratedDatabaseV9.playlistDAO().all.blockingFirst()
var remoteListFromDB = migratedDatabaseV9.playlistRemoteDAO().all.blockingFirst()
var localListFromDB = migratedDatabaseV9.playlistDAO().getAll().blockingFirst()
var remoteListFromDB = migratedDatabaseV9.playlistRemoteDAO().getAll().blockingFirst()
assertEquals(1, localListFromDB.size)
assertEquals(localUid2, localListFromDB[0].uid)
@ -303,8 +303,8 @@ class DatabaseMigrationTest {
)
)
localListFromDB = migratedDatabaseV9.playlistDAO().all.blockingFirst()
remoteListFromDB = migratedDatabaseV9.playlistRemoteDAO().all.blockingFirst()
localListFromDB = migratedDatabaseV9.playlistDAO().getAll().blockingFirst()
remoteListFromDB = migratedDatabaseV9.playlistRemoteDAO().getAll().blockingFirst()
assertEquals(2, localListFromDB.size)
assertEquals(localUid3, localListFromDB[1].uid)
assertEquals(-1, localListFromDB[1].displayIndex)

View File

@ -3,7 +3,7 @@ package org.schabi.newpipe.database
import android.content.Context
import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import io.reactivex.rxjava3.core.Single
import kotlinx.coroutines.runBlocking
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
@ -22,7 +22,6 @@ import org.schabi.newpipe.extractor.channel.ChannelInfo
import org.schabi.newpipe.extractor.stream.StreamType
import java.io.IOException
import java.time.OffsetDateTime
import kotlin.streams.toList
class FeedDAOTest {
private lateinit var db: AppDatabase
@ -94,17 +93,13 @@ class FeedDAOTest {
)
}
private fun setupUnlinkDelete(time: String) {
private fun setupUnlinkDelete(time: String) = runBlocking {
clearAndFillTables()
Single.fromCallable {
feedDAO.unlinkStreamsOlderThan(OffsetDateTime.parse(time))
}.blockingSubscribe()
Single.fromCallable {
streamDAO.deleteOrphans()
}.blockingSubscribe()
feedDAO.unlinkStreamsOlderThan(OffsetDateTime.parse(time))
streamDAO.deleteOrphans()
}
private fun clearAndFillTables() {
private suspend fun clearAndFillTables() {
db.clearAllTables()
streamDAO.insertAll(allStreams)
subscriptionDAO.insertAll(

View File

@ -41,7 +41,7 @@ class HistoryRecordManagerTest {
// For some reason the Flowable returned by getAll() never completes, so we can't assert
// that the number of Lists it returns is exactly 1, we can only check if the first List is
// correct. Why on earth has a Flowable been used instead of a Single for getAll()?!?
val entities = database.searchHistoryDAO().all.blockingFirst()
val entities = database.searchHistoryDAO().getAll().blockingFirst()
assertThat(entities).hasSize(1)
assertThat(entities[0].id).isEqualTo(1)
assertThat(entities[0].serviceId).isEqualTo(0)
@ -59,25 +59,25 @@ class HistoryRecordManagerTest {
// make sure all 4 were inserted
database.searchHistoryDAO().insertAll(entries)
assertThat(database.searchHistoryDAO().all.blockingFirst()).hasSameSizeAs(entries)
assertThat(database.searchHistoryDAO().getAll().blockingFirst()).hasSameSizeAs(entries)
// try to delete only "A" entries, "B" entries should be untouched
manager.deleteSearchHistory("A").test().await().assertValue(2)
val entities = database.searchHistoryDAO().all.blockingFirst()
val entities = database.searchHistoryDAO().getAll().blockingFirst()
assertThat(entities).hasSize(2)
assertThat(entities).usingElementComparator { o1, o2 -> if (o1.hasEqualValues(o2)) 0 else 1 }
.containsExactly(*entries.subList(2, 4).toTypedArray())
// assert that nothing happens if we delete a search query that does exist in the db
manager.deleteSearchHistory("A").test().await().assertValue(0)
val entities2 = database.searchHistoryDAO().all.blockingFirst()
val entities2 = database.searchHistoryDAO().getAll().blockingFirst()
assertThat(entities2).hasSize(2)
assertThat(entities2).usingElementComparator { o1, o2 -> if (o1.hasEqualValues(o2)) 0 else 1 }
.containsExactly(*entries.subList(2, 4).toTypedArray())
// delete all remaining entries
manager.deleteSearchHistory("B").test().await().assertValue(2)
assertThat(database.searchHistoryDAO().all.blockingFirst()).isEmpty()
assertThat(database.searchHistoryDAO().getAll().blockingFirst()).isEmpty()
}
@Test
@ -90,11 +90,11 @@ class HistoryRecordManagerTest {
// make sure all 3 were inserted
database.searchHistoryDAO().insertAll(entries)
assertThat(database.searchHistoryDAO().all.blockingFirst()).hasSameSizeAs(entries)
assertThat(database.searchHistoryDAO().getAll().blockingFirst()).hasSameSizeAs(entries)
// should remove everything
manager.deleteCompleteSearchHistory().test().await().assertValue(entries.size)
assertThat(database.searchHistoryDAO().all.blockingFirst()).isEmpty()
assertThat(database.searchHistoryDAO().getAll().blockingFirst()).isEmpty()
}
private fun insertShuffledRelatedSearches(relatedSearches: Collection<SearchHistoryEntry>) {
@ -107,7 +107,7 @@ class HistoryRecordManagerTest {
// make sure all entries were inserted
assertEquals(
relatedSearches.size,
database.searchHistoryDAO().all.blockingFirst().size
database.searchHistoryDAO().getAll().blockingFirst().size
)
}

View File

@ -72,6 +72,6 @@ class LocalPlaylistManagerTest {
val result = manager.createPlaylist("name", listOf(stream, upserted))
result.test().await().assertComplete()
database.streamDAO().all.test().awaitCount(1).assertValue(listOf(stream, upserted))
database.streamDAO().getAll().test().awaitCount(1).assertValue(listOf(stream, upserted))
}
}

View File

@ -1,5 +1,6 @@
package org.schabi.newpipe;
import android.app.ActivityManager;
import android.app.Application;
import android.content.Context;
import android.content.SharedPreferences;
@ -8,6 +9,7 @@ import android.util.Log;
import androidx.annotation.NonNull;
import androidx.core.app.NotificationChannelCompat;
import androidx.core.app.NotificationManagerCompat;
import androidx.core.content.ContextCompat;
import androidx.preference.PreferenceManager;
import com.jakewharton.processphoenix.ProcessPhoenix;
@ -20,18 +22,23 @@ import org.schabi.newpipe.extractor.downloader.Downloader;
import org.schabi.newpipe.ktx.ExceptionUtils;
import org.schabi.newpipe.settings.NewPipeSettings;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.image.ImageStrategy;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.ServiceHelper;
import org.schabi.newpipe.util.StateSaver;
import org.schabi.newpipe.util.image.ImageStrategy;
import org.schabi.newpipe.util.image.PreferredImageQuality;
import java.io.File;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.SocketException;
import java.util.List;
import java.util.Objects;
import coil.ImageLoader;
import coil.ImageLoaderFactory;
import coil.disk.DiskCache;
import coil.memory.MemoryCache;
import coil.util.DebugLogger;
import io.reactivex.rxjava3.exceptions.CompositeException;
import io.reactivex.rxjava3.exceptions.MissingBackpressureException;
import io.reactivex.rxjava3.exceptions.OnErrorNotImplementedException;
@ -57,7 +64,7 @@ import io.reactivex.rxjava3.plugins.RxJavaPlugins;
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public class App extends Application {
public class App extends Application implements ImageLoaderFactory {
public static final String PACKAGE_NAME = BuildConfig.APPLICATION_ID;
private static final String TAG = App.class.toString();
@ -108,20 +115,33 @@ public class App extends Application {
// Initialize image loader
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
PicassoHelper.init(this);
ImageStrategy.setPreferredImageQuality(PreferredImageQuality.fromPreferenceKey(this,
prefs.getString(getString(R.string.image_quality_key),
getString(R.string.image_quality_default))));
PicassoHelper.setIndicatorsEnabled(MainActivity.DEBUG
&& prefs.getBoolean(getString(R.string.show_image_indicators_key), false));
configureRxJavaErrorHandler();
}
@NonNull
@Override
public void onTerminate() {
super.onTerminate();
PicassoHelper.terminate();
public ImageLoader newImageLoader() {
final var isLowRamDevice = ContextCompat.getSystemService(this, ActivityManager.class)
.isLowRamDevice();
final var builder = new ImageLoader.Builder(this)
.memoryCache(() -> new MemoryCache.Builder(this)
.maxSizeBytes(10 * 1024 * 1024)
.build())
.diskCache(() -> new DiskCache.Builder()
.directory(new File(getExternalCacheDir(), "coil"))
.maxSizeBytes(50 * 1024 * 1024)
.build())
.allowRgb565(isLowRamDevice);
if (MainActivity.DEBUG) {
builder.logger(new DebugLogger());
}
return builder.build();
}
protected Downloader getDownloader() {

View File

@ -167,8 +167,8 @@ class AboutActivity : AppCompatActivity() {
"https://square.github.io/okhttp/", StandardLicenses.APACHE2
),
SoftwareComponent(
"Picasso", "2013", "Square, Inc.",
"https://square.github.io/picasso/", StandardLicenses.APACHE2
"Coil", "2023", "Coil Contributors",
"https://coil-kt.github.io/coil/", StandardLicenses.APACHE2
),
SoftwareComponent(
"PrettyTime", "2012 - 2020", "Lincoln Baxter, III",

View File

@ -1,39 +0,0 @@
package org.schabi.newpipe.database;
import androidx.room.Dao;
import androidx.room.Delete;
import androidx.room.Insert;
import androidx.room.Update;
import java.util.Collection;
import java.util.List;
import io.reactivex.rxjava3.core.Flowable;
@Dao
public interface BasicDAO<Entity> {
/* Inserts */
@Insert
long insert(Entity entity);
@Insert
List<Long> insertAll(Collection<Entity> entities);
/* Searches */
Flowable<List<Entity>> getAll();
Flowable<List<Entity>> listByService(int serviceId);
/* Deletes */
@Delete
void delete(Entity entity);
int deleteAll();
/* Updates */
@Update
int update(Entity entity);
@Update
void update(Collection<Entity> entities);
}

View File

@ -0,0 +1,32 @@
package org.schabi.newpipe.database
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Update
import io.reactivex.rxjava3.core.Flowable
@Dao
interface BasicDAO<Entity> {
/* Inserts */
@Insert
fun insert(entity: Entity): Long
@Insert
fun insertAll(entities: Collection<Entity>): List<Long>
/* Searches */
fun getAll(): Flowable<List<Entity>>
fun listByService(serviceId: Int): Flowable<List<Entity>>
/* Deletes */
@Delete
fun delete(entity: Entity)
fun deleteAll(): Int
/* Updates */
@Update
fun update(entity: Entity): Int
}

View File

@ -8,6 +8,7 @@ import androidx.room.Transaction
import androidx.room.Update
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.core.Maybe
import kotlinx.coroutines.flow.Flow
import org.schabi.newpipe.database.feed.model.FeedEntity
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity
@ -18,9 +19,9 @@ import org.schabi.newpipe.database.subscription.SubscriptionEntity
import java.time.OffsetDateTime
@Dao
abstract class FeedDAO {
interface FeedDAO {
@Query("DELETE FROM feed")
abstract fun deleteAll(): Int
suspend fun deleteAll(): Int
/**
* @param groupId the group id to get feed streams of; use
@ -86,7 +87,7 @@ abstract class FeedDAO {
LIMIT 500
"""
)
abstract fun getStreams(
fun getStreams(
groupId: Long,
includePlayed: Boolean,
includePartiallyPlayed: Boolean,
@ -119,7 +120,7 @@ abstract class FeedDAO {
AND s.upload_date <> max_upload_date))
"""
)
abstract fun unlinkStreamsOlderThan(offsetDateTime: OffsetDateTime)
suspend fun unlinkStreamsOlderThan(offsetDateTime: OffsetDateTime)
@Query(
"""
@ -137,22 +138,19 @@ abstract class FeedDAO {
)
"""
)
abstract fun unlinkOldLivestreams(subscriptionId: Long)
suspend fun unlinkOldLivestreams(subscriptionId: Long)
@Insert(onConflict = OnConflictStrategy.IGNORE)
abstract fun insert(feedEntity: FeedEntity)
suspend fun insertAll(entities: List<FeedEntity>): List<Long>
@Insert(onConflict = OnConflictStrategy.IGNORE)
abstract fun insertAll(entities: List<FeedEntity>): List<Long>
@Insert(onConflict = OnConflictStrategy.IGNORE)
internal abstract fun insertLastUpdated(lastUpdatedEntity: FeedLastUpdatedEntity): Long
suspend fun insertLastUpdated(lastUpdatedEntity: FeedLastUpdatedEntity): Long
@Update(onConflict = OnConflictStrategy.IGNORE)
internal abstract fun updateLastUpdated(lastUpdatedEntity: FeedLastUpdatedEntity)
suspend fun updateLastUpdated(lastUpdatedEntity: FeedLastUpdatedEntity)
@Transaction
open fun setLastUpdatedForSubscription(lastUpdatedEntity: FeedLastUpdatedEntity) {
suspend fun setLastUpdatedForSubscription(lastUpdatedEntity: FeedLastUpdatedEntity) {
val id = insertLastUpdated(lastUpdatedEntity)
if (id == -1L) {
@ -168,13 +166,13 @@ abstract class FeedDAO {
ON fgs.subscription_id = lu.subscription_id AND fgs.group_id = :groupId
"""
)
abstract fun oldestSubscriptionUpdate(groupId: Long): Flowable<List<OffsetDateTime>>
fun oldestSubscriptionUpdate(groupId: Long): Flowable<List<OffsetDateTime>>
@Query("SELECT MIN(last_updated) FROM feed_last_updated")
abstract fun oldestSubscriptionUpdateFromAll(): Flowable<List<OffsetDateTime>>
fun oldestSubscriptionUpdateFromAll(): Flowable<List<OffsetDateTime>>
@Query("SELECT COUNT(*) FROM feed_last_updated WHERE last_updated IS NULL")
abstract fun notLoadedCount(): Flowable<Long>
fun notLoadedCount(): Flowable<Long>
@Query(
"""
@ -189,7 +187,7 @@ abstract class FeedDAO {
WHERE lu.last_updated IS NULL
"""
)
abstract fun notLoadedCountForGroup(groupId: Long): Flowable<Long>
fun notLoadedCountForGroup(groupId: Long): Flowable<Long>
@Query(
"""
@ -201,7 +199,7 @@ abstract class FeedDAO {
WHERE lu.last_updated IS NULL OR lu.last_updated < :outdatedThreshold
"""
)
abstract fun getAllOutdated(outdatedThreshold: OffsetDateTime): Flowable<List<SubscriptionEntity>>
fun getAllOutdated(outdatedThreshold: OffsetDateTime): Flow<List<SubscriptionEntity>>
@Query(
"""
@ -216,7 +214,7 @@ abstract class FeedDAO {
WHERE lu.last_updated IS NULL OR lu.last_updated < :outdatedThreshold
"""
)
abstract fun getAllOutdatedForGroup(groupId: Long, outdatedThreshold: OffsetDateTime): Flowable<List<SubscriptionEntity>>
fun getAllOutdatedForGroup(groupId: Long, outdatedThreshold: OffsetDateTime): Flow<List<SubscriptionEntity>>
@Query(
"""
@ -230,8 +228,8 @@ abstract class FeedDAO {
AND s.notification_mode = :notificationMode
"""
)
abstract fun getOutdatedWithNotificationMode(
fun getOutdatedWithNotificationMode(
outdatedThreshold: OffsetDateTime,
@NotificationMode notificationMode: Int
): Flowable<List<SubscriptionEntity>>
): Flow<List<SubscriptionEntity>>
}

View File

@ -1,5 +1,11 @@
package org.schabi.newpipe.database.history.dao;
import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.CREATION_DATE;
import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.ID;
import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.SEARCH;
import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.SERVICE_ID;
import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.TABLE_NAME;
import androidx.annotation.Nullable;
import androidx.room.Dao;
import androidx.room.Query;
@ -10,12 +16,6 @@ import java.util.List;
import io.reactivex.rxjava3.core.Flowable;
import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.CREATION_DATE;
import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.ID;
import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.SEARCH;
import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.SERVICE_ID;
import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.TABLE_NAME;
@Dao
public interface SearchHistoryDAO extends HistoryDAO<SearchHistoryEntry> {
String ORDER_BY_CREATION_DATE = " ORDER BY " + CREATION_DATE + " DESC";

View File

@ -1,5 +1,8 @@
package org.schabi.newpipe.database.playlist.dao;
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_ID;
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_TABLE;
import androidx.room.Dao;
import androidx.room.Query;
import androidx.room.Transaction;
@ -11,9 +14,6 @@ import java.util.List;
import io.reactivex.rxjava3.core.Flowable;
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_ID;
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_TABLE;
@Dao
public interface PlaylistDAO extends BasicDAO<PlaylistEntity> {
@Override

View File

@ -1,5 +1,11 @@
package org.schabi.newpipe.database.playlist.dao;
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_DISPLAY_INDEX;
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_ID;
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_SERVICE_ID;
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_TABLE;
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_URL;
import androidx.room.Dao;
import androidx.room.Query;
import androidx.room.Transaction;
@ -11,12 +17,6 @@ import java.util.List;
import io.reactivex.rxjava3.core.Flowable;
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_DISPLAY_INDEX;
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_ID;
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_SERVICE_ID;
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_TABLE;
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_URL;
@Dao
public interface PlaylistRemoteDAO extends BasicDAO<PlaylistRemoteEntity> {
@Override

View File

@ -8,6 +8,7 @@ import androidx.room.Query
import androidx.room.Transaction
import io.reactivex.rxjava3.core.Completable
import io.reactivex.rxjava3.core.Flowable
import kotlinx.coroutines.runBlocking
import org.schabi.newpipe.database.BasicDAO
import org.schabi.newpipe.database.stream.model.StreamEntity
import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_ID
@ -16,30 +17,27 @@ import org.schabi.newpipe.util.StreamTypeUtil
import java.time.OffsetDateTime
@Dao
abstract class StreamDAO : BasicDAO<StreamEntity> {
interface StreamDAO : BasicDAO<StreamEntity> {
@Query("SELECT * FROM streams")
abstract override fun getAll(): Flowable<List<StreamEntity>>
override fun getAll(): Flowable<List<StreamEntity>>
@Query("DELETE FROM streams")
abstract override fun deleteAll(): Int
override fun deleteAll(): Int
@Query("SELECT * FROM streams WHERE service_id = :serviceId")
abstract override fun listByService(serviceId: Int): Flowable<List<StreamEntity>>
override fun listByService(serviceId: Int): Flowable<List<StreamEntity>>
@Query("SELECT * FROM streams WHERE url = :url AND service_id = :serviceId")
abstract fun getStream(serviceId: Long, url: String): Flowable<List<StreamEntity>>
fun getStream(serviceId: Long, url: String): Flowable<List<StreamEntity>>
@Query("UPDATE streams SET uploader_url = :uploaderUrl WHERE url = :url AND service_id = :serviceId")
abstract fun setUploaderUrl(serviceId: Long, url: String, uploaderUrl: String): Completable
fun setUploaderUrl(serviceId: Long, url: String, uploaderUrl: String): Completable
@Insert(onConflict = OnConflictStrategy.IGNORE)
internal abstract fun silentInsertInternal(stream: StreamEntity): Long
@Insert(onConflict = OnConflictStrategy.IGNORE)
internal abstract fun silentInsertAllInternal(streams: List<StreamEntity>): List<Long>
fun silentInsertInternal(stream: StreamEntity): Long
@Query("SELECT COUNT(*) != 0 FROM streams WHERE url = :url AND service_id = :serviceId")
internal abstract fun exists(serviceId: Int, url: String): Boolean
suspend fun exists(serviceId: Int, url: String): Boolean
@Query(
"""
@ -47,10 +45,10 @@ abstract class StreamDAO : BasicDAO<StreamEntity> {
FROM streams WHERE url = :url AND service_id = :serviceId
"""
)
internal abstract fun getMinimalStreamForCompare(serviceId: Int, url: String): StreamCompareFeed?
fun getMinimalStreamForCompare(serviceId: Int, url: String): StreamCompareFeed?
@Transaction
open fun upsert(newerStream: StreamEntity): Long {
fun upsert(newerStream: StreamEntity): Long {
val uid = silentInsertInternal(newerStream)
if (uid != -1L) {
@ -65,24 +63,8 @@ abstract class StreamDAO : BasicDAO<StreamEntity> {
}
@Transaction
open fun upsertAll(streams: List<StreamEntity>): List<Long> {
val insertUidList = silentInsertAllInternal(streams)
val streamIds = ArrayList<Long>(streams.size)
for ((index, uid) in insertUidList.withIndex()) {
val newerStream = streams[index]
if (uid != -1L) {
streamIds.add(uid)
newerStream.uid = uid
continue
}
compareAndUpdateStream(newerStream)
streamIds.add(newerStream.uid)
}
update(streams)
return streamIds
fun upsertAll(streams: List<StreamEntity>): List<Long> {
return streams.map { upsert(it) }
}
private fun compareAndUpdateStream(newerStream: StreamEntity) {
@ -91,7 +73,6 @@ abstract class StreamDAO : BasicDAO<StreamEntity> {
newerStream.uid = existentMinimalStream.uid
if (!StreamTypeUtil.isLiveStream(newerStream.streamType)) {
// Use the existent upload date if the newer stream does not have a better precision
// (i.e. is an approximation). This is done to prevent unnecessary changes.
val hasBetterPrecision =
@ -122,12 +103,16 @@ abstract class StreamDAO : BasicDAO<StreamEntity> {
WHERE f.stream_id = streams.uid)
"""
)
abstract fun deleteOrphans(): Int
suspend fun deleteOrphans(): Int
fun deleteOrphansBlocking() = runBlocking {
deleteOrphans()
}
/**
* Minimal entry class used when comparing/updating an existent stream.
*/
internal data class StreamCompareFeed(
data class StreamCompareFeed(
@ColumnInfo(name = STREAM_ID)
var uid: Long = 0,

View File

@ -1,5 +1,8 @@
package org.schabi.newpipe.database.stream.dao;
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID;
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE;
import androidx.room.Dao;
import androidx.room.Insert;
import androidx.room.OnConflictStrategy;
@ -13,9 +16,6 @@ import java.util.List;
import io.reactivex.rxjava3.core.Flowable;
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID;
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE;
@Dao
public interface StreamStateDAO extends BasicDAO<StreamStateEntity> {
@Override

View File

@ -6,20 +6,25 @@ import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.RewriteQueriesToDropUnusedColumns
import androidx.room.Transaction
import androidx.room.Update
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.core.Maybe
import org.schabi.newpipe.database.BasicDAO
@Dao
abstract class SubscriptionDAO : BasicDAO<SubscriptionEntity> {
interface SubscriptionDAO : BasicDAO<SubscriptionEntity> {
// TODO: Replace with the standard update method when migrating to suspend functions
@Update
suspend fun updateSubscription(entity: SubscriptionEntity)
@Query("SELECT COUNT(*) FROM subscriptions")
abstract fun rowCount(): Flowable<Long>
fun rowCount(): Flowable<Long>
@Query("SELECT * FROM subscriptions WHERE service_id = :serviceId")
abstract override fun listByService(serviceId: Int): Flowable<List<SubscriptionEntity>>
override fun listByService(serviceId: Int): Flowable<List<SubscriptionEntity>>
@Query("SELECT * FROM subscriptions ORDER BY name COLLATE NOCASE ASC")
abstract override fun getAll(): Flowable<List<SubscriptionEntity>>
override fun getAll(): Flowable<List<SubscriptionEntity>>
@Query(
"""
@ -30,7 +35,7 @@ abstract class SubscriptionDAO : BasicDAO<SubscriptionEntity> {
ORDER BY name COLLATE NOCASE ASC
"""
)
abstract fun getSubscriptionsFiltered(filter: String): Flowable<List<SubscriptionEntity>>
fun getSubscriptionsFiltered(filter: String): Flowable<List<SubscriptionEntity>>
@RewriteQueriesToDropUnusedColumns
@Query(
@ -45,7 +50,7 @@ abstract class SubscriptionDAO : BasicDAO<SubscriptionEntity> {
ORDER BY name COLLATE NOCASE ASC
"""
)
abstract fun getSubscriptionsOnlyUngrouped(
fun getSubscriptionsOnlyUngrouped(
currentGroupId: Long
): Flowable<List<SubscriptionEntity>>
@ -63,34 +68,34 @@ abstract class SubscriptionDAO : BasicDAO<SubscriptionEntity> {
ORDER BY name COLLATE NOCASE ASC
"""
)
abstract fun getSubscriptionsOnlyUngroupedFiltered(
fun getSubscriptionsOnlyUngroupedFiltered(
currentGroupId: Long,
filter: String
): Flowable<List<SubscriptionEntity>>
@Query("SELECT * FROM subscriptions WHERE url LIKE :url AND service_id = :serviceId")
abstract fun getSubscriptionFlowable(serviceId: Int, url: String): Flowable<List<SubscriptionEntity>>
fun getSubscriptionFlowable(serviceId: Int, url: String): Flowable<List<SubscriptionEntity>>
@Query("SELECT * FROM subscriptions WHERE url LIKE :url AND service_id = :serviceId")
abstract fun getSubscription(serviceId: Int, url: String): Maybe<SubscriptionEntity>
fun getSubscription(serviceId: Int, url: String): Maybe<SubscriptionEntity>
@Query("SELECT * FROM subscriptions WHERE uid = :subscriptionId")
abstract fun getSubscription(subscriptionId: Long): SubscriptionEntity
suspend fun getSubscription(subscriptionId: Long): SubscriptionEntity
@Query("DELETE FROM subscriptions")
abstract override fun deleteAll(): Int
override fun deleteAll(): Int
@Query("DELETE FROM subscriptions WHERE url LIKE :url AND service_id = :serviceId")
abstract fun deleteSubscription(serviceId: Int, url: String): Int
fun deleteSubscription(serviceId: Int, url: String): Int
@Query("SELECT uid FROM subscriptions WHERE url LIKE :url AND service_id = :serviceId")
internal abstract fun getSubscriptionIdInternal(serviceId: Int, url: String): Long?
suspend fun getSubscriptionIdInternal(serviceId: Int, url: String): Long?
@Insert(onConflict = OnConflictStrategy.IGNORE)
internal abstract fun silentInsertAllInternal(entities: List<SubscriptionEntity>): List<Long>
suspend fun silentInsertAllInternal(entities: List<SubscriptionEntity>): List<Long>
@Transaction
open fun upsertAll(entities: List<SubscriptionEntity>): List<SubscriptionEntity> {
suspend fun upsertAll(entities: List<SubscriptionEntity>): List<SubscriptionEntity> {
val insertUidList = silentInsertAllInternal(entities)
insertUidList.forEachIndexed { index: Int, uidFromInsert: Long ->
@ -103,7 +108,7 @@ abstract class SubscriptionDAO : BasicDAO<SubscriptionEntity> {
?: throw IllegalStateException("Subscription cannot be null just after insertion.")
entity.uid = subscriptionIdFromDb
update(entity)
updateSubscription(entity)
}
}

View File

@ -116,7 +116,7 @@ import org.schabi.newpipe.util.StreamTypeUtil;
import org.schabi.newpipe.util.ThemeHelper;
import org.schabi.newpipe.util.external_communication.KoreUtils;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.image.CoilHelper;
import java.util.ArrayList;
import java.util.Iterator;
@ -127,6 +127,7 @@ import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import coil.util.CoilUtils;
import icepick.State;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
@ -159,8 +160,6 @@ public final class VideoDetailFragment
private static final String DESCRIPTION_TAB_TAG = "DESCRIPTION TAB";
private static final String EMPTY_TAB_TAG = "EMPTY TAB";
private static final String PICASSO_VIDEO_DETAILS_TAG = "PICASSO_VIDEO_DETAILS_TAG";
// tabs
private boolean showComments;
private boolean showRelatedItems;
@ -1471,7 +1470,10 @@ public final class VideoDetailFragment
}
}
PicassoHelper.cancelTag(PICASSO_VIDEO_DETAILS_TAG);
CoilUtils.dispose(binding.detailThumbnailImageView);
CoilUtils.dispose(binding.detailSubChannelThumbnailView);
CoilUtils.dispose(binding.overlayThumbnail);
binding.detailThumbnailImageView.setImageBitmap(null);
binding.detailSubChannelThumbnailView.setImageBitmap(null);
}
@ -1562,8 +1564,8 @@ public final class VideoDetailFragment
binding.detailSecondaryControlPanel.setVisibility(View.GONE);
checkUpdateProgressInfo(info);
PicassoHelper.loadDetailsThumbnail(info.getThumbnails()).tag(PICASSO_VIDEO_DETAILS_TAG)
.into(binding.detailThumbnailImageView);
CoilHelper.INSTANCE.loadDetailsThumbnail(binding.detailThumbnailImageView,
info.getThumbnails());
showMetaInfoInTextView(info.getMetaInfo(), binding.detailMetaInfoTextView,
binding.detailMetaInfoSeparator, disposables);
@ -1613,8 +1615,8 @@ public final class VideoDetailFragment
binding.detailUploaderTextView.setVisibility(View.GONE);
}
PicassoHelper.loadAvatar(info.getUploaderAvatars()).tag(PICASSO_VIDEO_DETAILS_TAG)
.into(binding.detailSubChannelThumbnailView);
CoilHelper.INSTANCE.loadAvatar(binding.detailSubChannelThumbnailView,
info.getUploaderAvatars());
binding.detailSubChannelThumbnailView.setVisibility(View.VISIBLE);
binding.detailUploaderThumbnailView.setVisibility(View.GONE);
}
@ -1645,11 +1647,11 @@ public final class VideoDetailFragment
binding.detailUploaderTextView.setVisibility(View.GONE);
}
PicassoHelper.loadAvatar(info.getSubChannelAvatars()).tag(PICASSO_VIDEO_DETAILS_TAG)
.into(binding.detailSubChannelThumbnailView);
CoilHelper.INSTANCE.loadAvatar(binding.detailSubChannelThumbnailView,
info.getSubChannelAvatars());
binding.detailSubChannelThumbnailView.setVisibility(View.VISIBLE);
PicassoHelper.loadAvatar(info.getUploaderAvatars()).tag(PICASSO_VIDEO_DETAILS_TAG)
.into(binding.detailUploaderThumbnailView);
CoilHelper.INSTANCE.loadAvatar(binding.detailUploaderThumbnailView,
info.getUploaderAvatars());
binding.detailUploaderThumbnailView.setVisibility(View.VISIBLE);
}
@ -2403,8 +2405,7 @@ public final class VideoDetailFragment
binding.overlayTitleTextView.setText(isEmpty(overlayTitle) ? "" : overlayTitle);
binding.overlayChannelTextView.setText(isEmpty(uploader) ? "" : uploader);
binding.overlayThumbnail.setImageDrawable(null);
PicassoHelper.loadDetailsThumbnail(thumbnails).tag(PICASSO_VIDEO_DETAILS_TAG)
.into(binding.overlayThumbnail);
CoilHelper.INSTANCE.loadDetailsThumbnail(binding.overlayThumbnail, thumbnails);
}
private void setOverlayPlayPauseImage(final boolean playerIsPlaying) {

View File

@ -49,15 +49,16 @@ import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.StateSaver;
import org.schabi.newpipe.util.image.ImageStrategy;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.ThemeHelper;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.util.image.CoilHelper;
import org.schabi.newpipe.util.image.ImageStrategy;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.TimeUnit;
import coil.util.CoilUtils;
import icepick.State;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Observable;
@ -72,7 +73,6 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
implements StateSaver.WriteRead {
private static final int BUTTON_DEBOUNCE_INTERVAL = 100;
private static final String PICASSO_CHANNEL_TAG = "PICASSO_CHANNEL_TAG";
@State
protected int serviceId = Constants.NO_SERVICE_ID;
@ -569,7 +569,9 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
@Override
public void showLoading() {
super.showLoading();
PicassoHelper.cancelTag(PICASSO_CHANNEL_TAG);
CoilUtils.dispose(binding.channelAvatarView);
CoilUtils.dispose(binding.channelBannerImage);
CoilUtils.dispose(binding.subChannelAvatarView);
animate(binding.channelSubscribeButton, false, 100);
}
@ -580,17 +582,15 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
setInitialData(result.getServiceId(), result.getOriginalUrl(), result.getName());
if (ImageStrategy.shouldLoadImages() && !result.getBanners().isEmpty()) {
PicassoHelper.loadBanner(result.getBanners()).tag(PICASSO_CHANNEL_TAG)
.into(binding.channelBannerImage);
CoilHelper.INSTANCE.loadBanner(binding.channelBannerImage, result.getBanners());
} else {
// do not waste space for the banner, if the user disabled images or there is not one
binding.channelBannerImage.setImageDrawable(null);
}
PicassoHelper.loadAvatar(result.getAvatars()).tag(PICASSO_CHANNEL_TAG)
.into(binding.channelAvatarView);
PicassoHelper.loadAvatar(result.getParentChannelAvatars()).tag(PICASSO_CHANNEL_TAG)
.into(binding.subChannelAvatarView);
CoilHelper.INSTANCE.loadAvatar(binding.channelAvatarView, result.getAvatars());
CoilHelper.INSTANCE.loadAvatar(binding.subChannelAvatarView,
result.getParentChannelAvatars());
binding.channelTitleView.setText(result.getName());
binding.channelSubscriberView.setVisibility(View.VISIBLE);

View File

@ -23,8 +23,8 @@ import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.image.CoilHelper;
import org.schabi.newpipe.util.image.ImageStrategy;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.text.TextLinkifier;
import java.util.Queue;
@ -82,7 +82,7 @@ public final class CommentRepliesFragment
final CommentsInfoItem item = commentsInfoItem;
// load the author avatar
PicassoHelper.loadAvatar(item.getUploaderAvatars()).into(binding.authorAvatar);
CoilHelper.INSTANCE.loadAvatar(binding.authorAvatar, item.getUploaderAvatars());
binding.authorAvatar.setVisibility(ImageStrategy.shouldLoadImages()
? View.VISIBLE : View.GONE);

View File

@ -53,7 +53,7 @@ import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.PlayButtonHelper;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.image.CoilHelper;
import org.schabi.newpipe.util.text.TextEllipsizer;
import java.util.ArrayList;
@ -62,6 +62,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import coil.util.CoilUtils;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Flowable;
import io.reactivex.rxjava3.core.Single;
@ -71,8 +72,6 @@ import io.reactivex.rxjava3.disposables.Disposable;
public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, PlaylistInfo>
implements PlaylistControlViewHolder {
private static final String PICASSO_PLAYLIST_TAG = "PICASSO_PLAYLIST_TAG";
private CompositeDisposable disposables;
private Subscription bookmarkReactor;
private AtomicBoolean isBookmarkButtonReady;
@ -276,7 +275,7 @@ public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, Playl
animate(headerBinding.getRoot(), false, 200);
animateHideRecyclerViewAllowingScrolling(itemsList);
PicassoHelper.cancelTag(PICASSO_PLAYLIST_TAG);
CoilUtils.dispose(headerBinding.uploaderAvatarView);
animate(headerBinding.uploaderLayout, false, 200);
}
@ -327,8 +326,8 @@ public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, Playl
R.drawable.ic_radio)
);
} else {
PicassoHelper.loadAvatar(result.getUploaderAvatars()).tag(PICASSO_PLAYLIST_TAG)
.into(headerBinding.uploaderAvatarView);
CoilHelper.INSTANCE.loadAvatar(headerBinding.uploaderAvatarView,
result.getUploaderAvatars());
}
streamCount = result.getStreamCount();

View File

@ -1,19 +1,18 @@
package org.schabi.newpipe.info_list
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import com.xwray.groupie.GroupieViewHolder
import com.xwray.groupie.Item
import com.xwray.groupie.viewbinding.BindableItem
import com.xwray.groupie.viewbinding.GroupieViewHolder
import org.schabi.newpipe.R
import org.schabi.newpipe.databinding.ItemStreamSegmentBinding
import org.schabi.newpipe.extractor.stream.StreamSegment
import org.schabi.newpipe.util.Localization
import org.schabi.newpipe.util.image.PicassoHelper
import org.schabi.newpipe.util.image.CoilHelper
class StreamSegmentItem(
private val item: StreamSegment,
private val onClick: StreamSegmentAdapter.StreamSegmentListener
) : Item<GroupieViewHolder>() {
) : BindableItem<ItemStreamSegmentBinding>() {
companion object {
const val PAYLOAD_SELECT = 1
@ -21,31 +20,32 @@ class StreamSegmentItem(
var isSelected = false
override fun bind(viewHolder: GroupieViewHolder, position: Int) {
item.previewUrl?.let {
PicassoHelper.loadThumbnail(it)
.into(viewHolder.root.findViewById<ImageView>(R.id.previewImage))
}
viewHolder.root.findViewById<TextView>(R.id.textViewTitle).text = item.title
override fun bind(viewBinding: ItemStreamSegmentBinding, position: Int) {
CoilHelper.loadThumbnail(viewBinding.previewImage, item.previewUrl)
viewBinding.textViewTitle.text = item.title
if (item.channelName == null) {
viewHolder.root.findViewById<TextView>(R.id.textViewChannel).visibility = View.GONE
viewBinding.textViewChannel.visibility = View.GONE
// When the channel name is displayed there is less space
// and thus the segment title needs to be only one line height.
// But when there is no channel name displayed, the title can be two lines long.
// The default maxLines value is set to 1 to display all elements in the AS preview,
viewHolder.root.findViewById<TextView>(R.id.textViewTitle).maxLines = 2
viewBinding.textViewTitle.maxLines = 2
} else {
viewHolder.root.findViewById<TextView>(R.id.textViewChannel).text = item.channelName
viewHolder.root.findViewById<TextView>(R.id.textViewChannel).visibility = View.VISIBLE
viewBinding.textViewChannel.text = item.channelName
viewBinding.textViewChannel.visibility = View.VISIBLE
}
viewHolder.root.findViewById<TextView>(R.id.textViewStartSeconds).text =
viewBinding.textViewStartSeconds.text =
Localization.getDurationString(item.startTimeSeconds.toLong())
viewHolder.root.setOnClickListener { onClick.onItemClick(this, item.startTimeSeconds) }
viewHolder.root.setOnLongClickListener { onClick.onItemLongClick(this, item.startTimeSeconds); true }
viewHolder.root.isSelected = isSelected
viewBinding.root.setOnClickListener { onClick.onItemClick(this, item.startTimeSeconds) }
viewBinding.root.setOnLongClickListener { onClick.onItemLongClick(this, item.startTimeSeconds); true }
viewBinding.root.isSelected = isSelected
}
override fun bind(viewHolder: GroupieViewHolder, position: Int, payloads: MutableList<Any>) {
override fun bind(
viewHolder: GroupieViewHolder<ItemStreamSegmentBinding>,
position: Int,
payloads: MutableList<Any>
) {
if (payloads.contains(PAYLOAD_SELECT)) {
viewHolder.root.isSelected = isSelected
return
@ -54,4 +54,6 @@ class StreamSegmentItem(
}
override fun getLayout() = R.layout.item_stream_segment
override fun initializeViewBinding(view: View) = ItemStreamSegmentBinding.bind(view)
}

View File

@ -13,8 +13,8 @@ import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
import org.schabi.newpipe.extractor.utils.Utils;
import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.image.CoilHelper;
public class ChannelMiniInfoItemHolder extends InfoItemHolder {
private final ImageView itemThumbnailView;
@ -56,7 +56,7 @@ public class ChannelMiniInfoItemHolder extends InfoItemHolder {
itemAdditionalDetailView.setText(getDetailLine(item));
}
PicassoHelper.loadAvatar(item.getThumbnails()).into(itemThumbnailView);
CoilHelper.INSTANCE.loadAvatar(itemThumbnailView, item.getThumbnails());
itemView.setOnClickListener(view -> {
if (itemBuilder.getOnChannelSelectedListener() != null) {

View File

@ -23,8 +23,8 @@ import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.util.image.CoilHelper;
import org.schabi.newpipe.util.image.ImageStrategy;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.text.CommentTextOnTouchListener;
import org.schabi.newpipe.util.text.TextEllipsizer;
@ -79,14 +79,12 @@ public class CommentInfoItemHolder extends InfoItemHolder {
@Override
public void updateFromItem(final InfoItem infoItem,
final HistoryRecordManager historyRecordManager) {
if (!(infoItem instanceof CommentsInfoItem)) {
if (!(infoItem instanceof CommentsInfoItem item)) {
return;
}
final CommentsInfoItem item = (CommentsInfoItem) infoItem;
// load the author avatar
PicassoHelper.loadAvatar(item.getUploaderAvatars()).into(itemThumbnailView);
CoilHelper.INSTANCE.loadAvatar(itemThumbnailView, item.getUploaderAvatars());
if (ImageStrategy.shouldLoadImages()) {
itemThumbnailView.setVisibility(View.VISIBLE);
itemRoot.setPadding(commentVerticalPadding, commentVerticalPadding,

View File

@ -9,8 +9,8 @@ import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.image.CoilHelper;
public class PlaylistMiniInfoItemHolder extends InfoItemHolder {
public final ImageView itemThumbnailView;
@ -46,7 +46,7 @@ public class PlaylistMiniInfoItemHolder extends InfoItemHolder {
.localizeStreamCountMini(itemStreamCountView.getContext(), item.getStreamCount()));
itemUploaderView.setText(item.getUploaderName());
PicassoHelper.loadPlaylistThumbnail(item.getThumbnails()).into(itemThumbnailView);
CoilHelper.INSTANCE.loadPlaylistThumbnail(itemThumbnailView, item.getThumbnails());
itemView.setOnClickListener(view -> {
if (itemBuilder.getOnPlaylistSelectedListener() != null) {

View File

@ -16,8 +16,8 @@ import org.schabi.newpipe.ktx.ViewUtils;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.DependentPreferenceHelper;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.StreamTypeUtil;
import org.schabi.newpipe.util.image.CoilHelper;
import org.schabi.newpipe.views.AnimatedProgressBar;
import java.util.concurrent.TimeUnit;
@ -87,7 +87,7 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
}
// Default thumbnail is shown on error, while loading and if the url is empty
PicassoHelper.loadThumbnail(item.getThumbnails()).into(itemThumbnailView);
CoilHelper.INSTANCE.loadThumbnail(itemThumbnailView, item.getThumbnails());
itemView.setOnClickListener(view -> {
if (itemBuilder.getOnStreamSelectedListener() != null) {

View File

@ -0,0 +1,13 @@
package org.schabi.newpipe.ktx
import android.graphics.Bitmap
import android.graphics.Rect
import androidx.core.graphics.BitmapCompat
@Suppress("NOTHING_TO_INLINE")
inline fun Bitmap.scale(
width: Int,
height: Int,
srcRect: Rect? = null,
scaleInLinearSpace: Boolean = true,
) = BitmapCompat.createScaledBitmap(this, width, height, srcRect, scaleInLinearSpace)

View File

@ -73,27 +73,22 @@ class FeedDatabaseManager(context: Context) {
outdatedThreshold: OffsetDateTime
) = feedTable.getAllOutdatedForGroup(groupId, outdatedThreshold)
fun markAsOutdated(subscriptionId: Long) = feedTable
suspend fun markAsOutdated(subscriptionId: Long) = feedTable
.setLastUpdatedForSubscription(FeedLastUpdatedEntity(subscriptionId, null))
fun doesStreamExist(stream: StreamInfoItem): Boolean {
suspend fun doesStreamExist(stream: StreamInfoItem): Boolean {
return streamTable.exists(stream.serviceId, stream.url)
}
fun upsertAll(
suspend fun upsertAll(
subscriptionId: Long,
items: List<StreamInfoItem>,
oldestAllowedDate: OffsetDateTime = FEED_OLDEST_ALLOWED_DATE
) {
val itemsToInsert = ArrayList<StreamInfoItem>()
loop@ for (streamItem in items) {
val uploadDate = streamItem.uploadDate
itemsToInsert += when {
uploadDate == null && streamItem.streamType == StreamType.LIVE_STREAM -> streamItem
uploadDate != null && uploadDate.offsetDateTime() >= oldestAllowedDate -> streamItem
else -> continue@loop
}
val itemsToInsert = items.filter {
val uploadDate = it.uploadDate
uploadDate == null && it.streamType == StreamType.LIVE_STREAM ||
uploadDate != null && uploadDate.offsetDateTime() >= oldestAllowedDate
}
feedTable.unlinkOldLivestreams(subscriptionId)
@ -111,12 +106,12 @@ class FeedDatabaseManager(context: Context) {
)
}
fun removeOrphansOrOlderStreams(oldestAllowedDate: OffsetDateTime = FEED_OLDEST_ALLOWED_DATE) {
suspend fun removeOrphansOrOlderStreams(oldestAllowedDate: OffsetDateTime = FEED_OLDEST_ALLOWED_DATE) {
feedTable.unlinkStreamsOlderThan(oldestAllowedDate)
streamTable.deleteOrphans()
}
fun clear() {
suspend fun clear() {
feedTable.deleteAll()
val deletedOrphans = streamTable.deleteOrphans()
if (DEBUG) {

View File

@ -41,6 +41,7 @@ import androidx.core.content.edit
import androidx.core.os.bundleOf
import androidx.core.view.isVisible
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
@ -49,10 +50,9 @@ import com.xwray.groupie.Item
import com.xwray.groupie.OnItemClickListener
import com.xwray.groupie.OnItemLongClickListener
import icepick.State
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.schedulers.Schedulers
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.launch
import org.schabi.newpipe.NewPipeDatabase
import org.schabi.newpipe.R
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
@ -253,7 +253,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
viewModel.getShowFutureItemsFromPreferences()
)
AlertDialog.Builder(context!!)
AlertDialog.Builder(requireContext())
.setTitle(R.string.feed_hide_streams_title)
.setMultiChoiceItems(dialogItems, checkedDialogItems) { _, which, isChecked ->
checkedDialogItems[which] = isChecked
@ -453,24 +453,20 @@ class FeedFragment : BaseStateFragment<FeedState>() {
if (t is FeedLoadService.RequestException &&
t.cause is ContentNotAvailableException
) {
disposables.add(
Single.fromCallable {
NewPipeDatabase.getInstance(requireContext()).subscriptionDAO()
.getSubscription(t.subscriptionId)
lifecycleScope.launch(
CoroutineExceptionHandler { _, throwable ->
Log.e(TAG, "Unable to process", throwable)
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ subscriptionEntity ->
handleFeedNotAvailable(
subscriptionEntity,
t.cause,
errors.subList(i + 1, errors.size)
)
},
{ throwable -> Log.e(TAG, "Unable to process", throwable) }
)
)
) {
val subscriptionEntity = NewPipeDatabase.getInstance(requireContext())
.subscriptionDAO()
.getSubscription(t.subscriptionId)
handleFeedNotAvailable(
subscriptionEntity,
t.cause,
errors.subList(i + 1, errors.size)
)
}
// this will be called on the remaining errors by handleFeedNotAvailable()
return@handleItemsErrors
}

View File

@ -19,7 +19,7 @@ import org.schabi.newpipe.extractor.stream.StreamType.POST_LIVE_STREAM
import org.schabi.newpipe.extractor.stream.StreamType.VIDEO_STREAM
import org.schabi.newpipe.util.Localization
import org.schabi.newpipe.util.StreamTypeUtil
import org.schabi.newpipe.util.image.PicassoHelper
import org.schabi.newpipe.util.image.CoilHelper
import java.util.concurrent.TimeUnit
import java.util.function.Consumer
@ -101,7 +101,7 @@ data class StreamItem(
viewBinding.itemProgressView.visibility = View.GONE
}
PicassoHelper.loadThumbnail(stream.thumbnailUrl).into(viewBinding.itemThumbnailView)
CoilHelper.loadThumbnail(viewBinding.itemThumbnailView, stream.thumbnailUrl)
if (itemVersion != ItemVersion.MINI) {
viewBinding.itemAdditionalDetails.text =

View File

@ -6,7 +6,6 @@ import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Build
import android.provider.Settings
@ -16,20 +15,17 @@ import androidx.core.app.PendingIntentCompat
import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService
import androidx.preference.PreferenceManager
import com.squareup.picasso.Picasso
import com.squareup.picasso.Target
import org.schabi.newpipe.R
import org.schabi.newpipe.extractor.stream.StreamInfoItem
import org.schabi.newpipe.local.feed.service.FeedUpdateInfo
import org.schabi.newpipe.util.NavigationHelper
import org.schabi.newpipe.util.image.PicassoHelper
import org.schabi.newpipe.util.image.CoilHelper
/**
* Helper for everything related to show notifications about new streams to the user.
*/
class NotificationHelper(val context: Context) {
private val manager = NotificationManagerCompat.from(context)
private val iconLoadingTargets = ArrayList<Target>()
/**
* Show notifications for new streams from a single channel. The individual notifications are
@ -38,7 +34,7 @@ class NotificationHelper(val context: Context) {
* Opening the summary notification will open the corresponding channel page. Opening the
* individual notifications will open the corresponding video.
*/
fun displayNewStreamsNotifications(data: FeedUpdateInfo) {
suspend fun displayNewStreamsNotifications(data: FeedUpdateInfo) {
val newStreams = data.newStreams
val summary = context.resources.getQuantityString(
R.plurals.new_streams, newStreams.size, newStreams.size
@ -68,51 +64,23 @@ class NotificationHelper(val context: Context) {
summaryBuilder.setStyle(style)
// open the channel page when clicking on the summary notification
val intent = NavigationHelper
.getChannelIntent(context, data.serviceId, data.url)
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
summaryBuilder.setContentIntent(
PendingIntentCompat.getActivity(
context,
data.pseudoId,
NavigationHelper
.getChannelIntent(context, data.serviceId, data.url)
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
0,
false
)
PendingIntentCompat.getActivity(context, data.pseudoId, intent, 0, false)
)
// a Target is like a listener for image loading events
val target = object : Target {
override fun onBitmapLoaded(bitmap: Bitmap, from: Picasso.LoadedFrom) {
// set channel icon only if there is actually one (for Android versions < 7.0)
summaryBuilder.setLargeIcon(bitmap)
val avatarIcon =
CoilHelper.loadBitmap(context, data.avatarUrl, R.drawable.ic_newpipe_triangle_white)
// Show individual stream notifications, set channel icon only if there is actually
// one
showStreamNotifications(newStreams, data.serviceId, bitmap)
// Show summary notification
manager.notify(data.pseudoId, summaryBuilder.build())
summaryBuilder.setLargeIcon(avatarIcon)
iconLoadingTargets.remove(this) // allow it to be garbage-collected
}
override fun onBitmapFailed(e: Exception, errorDrawable: Drawable) {
// Show individual stream notifications
showStreamNotifications(newStreams, data.serviceId, null)
// Show summary notification
manager.notify(data.pseudoId, summaryBuilder.build())
iconLoadingTargets.remove(this) // allow it to be garbage-collected
}
override fun onPrepareLoad(placeHolderDrawable: Drawable) {
// Nothing to do
}
}
// add the target to the list to hold a strong reference and prevent it from being garbage
// collected, since Picasso only holds weak references to targets
iconLoadingTargets.add(target)
PicassoHelper.loadNotificationIcon(data.avatarUrl).into(target)
// Show individual stream notifications, set channel icon only if there is actually
// one
showStreamNotifications(newStreams, data.serviceId, avatarIcon)
// Show summary notification
manager.notify(data.pseudoId, summaryBuilder.build())
}
private fun showStreamNotifications(

View File

@ -4,6 +4,7 @@ import android.content.Context
import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.work.Constraints
import androidx.work.CoroutineWorker
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.ForegroundInfo
import androidx.work.NetworkType
@ -11,9 +12,6 @@ import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.PeriodicWorkRequest
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import androidx.work.rxjava3.RxWorker
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Single
import org.schabi.newpipe.App
import org.schabi.newpipe.R
import org.schabi.newpipe.error.ErrorInfo
@ -30,46 +28,38 @@ import java.util.concurrent.TimeUnit
class NotificationWorker(
appContext: Context,
workerParams: WorkerParameters,
) : RxWorker(appContext, workerParams) {
) : CoroutineWorker(appContext, workerParams) {
private val notificationHelper by lazy {
NotificationHelper(appContext)
}
private val feedLoadManager = FeedLoadManager(appContext)
override fun createWork(): Single<Result> = if (areNotificationsEnabled(applicationContext)) {
feedLoadManager.startLoading(
ignoreOutdatedThreshold = true,
groupId = FeedLoadManager.GROUP_NOTIFICATION_ENABLED
)
.doOnSubscribe { showLoadingFeedForegroundNotification() }
.map { feed ->
// filter out feedUpdateInfo items (i.e. channels) with nothing new
feed.mapNotNull {
it.value?.takeIf { feedUpdateInfo ->
feedUpdateInfo.newStreams.isNotEmpty()
}
}
}
.observeOn(AndroidSchedulers.mainThread()) // Picasso requires calls from main thread
.map { feedUpdateInfoList ->
override suspend fun doWork(): Result {
return if (areNotificationsEnabled(applicationContext)) {
try {
showLoadingFeedForegroundNotification()
val feedUpdateInfoList = feedLoadManager
.startLoading(FeedLoadManager.GROUP_NOTIFICATION_ENABLED, true)
// filter out feedUpdateInfo items (i.e. channels) with nothing new
.mapNotNull { it.value?.takeIf { it.newStreams.isNotEmpty() } }
// display notifications for each feedUpdateInfo (i.e. channel)
feedUpdateInfoList.forEach { feedUpdateInfo ->
notificationHelper.displayNewStreamsNotifications(feedUpdateInfo)
feedUpdateInfoList.forEach {
notificationHelper.displayNewStreamsNotifications(it)
}
return@map Result.success()
}
.doOnError { throwable ->
Log.e(TAG, "Error while displaying streams notifications", throwable)
Result.success()
} catch (e: Exception) {
Log.e(TAG, "Error while displaying streams notifications", e)
ErrorUtil.createNotification(
applicationContext,
ErrorInfo(throwable, UserAction.NEW_STREAMS_NOTIFICATIONS, "main worker")
ErrorInfo(e, UserAction.NEW_STREAMS_NOTIFICATIONS, "main worker")
)
Result.failure()
}
.onErrorReturnItem(Result.failure())
} else {
// the user can disable streams notifications in the device's app settings
Single.just(Result.success())
} else {
Result.success()
}
}
private fun showLoadingFeedForegroundNotification() {

View File

@ -3,14 +3,20 @@ package org.schabi.newpipe.local.feed.service
import android.content.Context
import android.content.SharedPreferences
import androidx.preference.PreferenceManager
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Completable
import androidx.room.withTransaction
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.core.Notification
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.functions.Consumer
import io.reactivex.rxjava3.processors.PublishProcessor
import io.reactivex.rxjava3.schedulers.Schedulers
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.buffer
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flatMapConcat
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.flow.takeWhile
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.withContext
import org.schabi.newpipe.R
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.database.subscription.NotificationMode
@ -56,10 +62,10 @@ class FeedLoadManager(private val context: Context) {
* within the `feed_update_threshold` are checked for updates. This threshold can be set by
* the user in the app settings. When `true`, all subscriptions are checked for new streams.
*/
fun startLoading(
suspend fun startLoading(
groupId: Long = FeedGroupEntity.GROUP_ALL_ID,
ignoreOutdatedThreshold: Boolean = false,
): Single<List<Notification<FeedUpdateInfo>>> {
): List<Notification<FeedUpdateInfo>> {
val defaultSharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
val useFeedExtractor = defaultSharedPreferences.getBoolean(
context.getString(R.string.feed_use_dedicated_fetch_method_key),
@ -91,36 +97,72 @@ class FeedLoadManager(private val context: Context) {
else -> feedDatabaseManager.outdatedSubscriptionsForGroup(groupId, outdatedThreshold)
}
return outdatedSubscriptions
.take(1)
.doOnNext {
currentProgress.set(0)
maxProgress.set(it.size)
val notifications = withContext(Dispatchers.IO) {
outdatedSubscriptions
.take(1)
.onEach {
currentProgress.set(0)
maxProgress.set(it.size)
}
.filter { it.isNotEmpty() }
.onEach {
notificationUpdater.onNext("")
broadcastProgress()
}
.takeWhile { !cancelSignal.get() }
.filter { !cancelSignal.get() }
.flatMapConcat { it ->
it.map { loadStreams(it, useFeedExtractor, defaultSharedPreferences) }
.asFlow()
}
.onEach {
currentProgress.incrementAndGet()
notificationUpdater.onNext(it.value?.name.orEmpty())
broadcastProgress()
}
.buffer(BUFFER_COUNT_BEFORE_INSERT)
.toList()
}
feedDatabaseManager.database().withTransaction {
for (notification in notifications) {
when {
notification.isOnNext -> {
val info = notification.value!!
notification.value!!.newStreams = filterNewStreams(info.streams)
feedDatabaseManager.upsertAll(info.uid, info.streams)
subscriptionManager.updateFromInfo(info)
if (info.errors.isNotEmpty()) {
feedResultsHolder.addErrors(
info.errors.map {
FeedLoadService.RequestException(
info.uid,
"${info.serviceId}:${info.url}",
it
)
}
)
feedDatabaseManager.markAsOutdated(info.uid)
}
}
notification.isOnError -> {
val error = notification.error
feedResultsHolder.addError(error!!)
if (error is FeedLoadService.RequestException) {
feedDatabaseManager.markAsOutdated(error.subscriptionId)
}
}
}
}
.filter { it.isNotEmpty() }
.observeOn(AndroidSchedulers.mainThread())
.doOnNext {
notificationUpdater.onNext("")
broadcastProgress()
}
.observeOn(Schedulers.io())
.flatMap { Flowable.fromIterable(it) }
.takeWhile { !cancelSignal.get() }
.parallel(PARALLEL_EXTRACTIONS, PARALLEL_EXTRACTIONS * 2)
.runOn(Schedulers.io(), PARALLEL_EXTRACTIONS * 2)
.filter { !cancelSignal.get() }
.map { subscriptionEntity ->
loadStreams(subscriptionEntity, useFeedExtractor, defaultSharedPreferences)
}
.sequential()
.observeOn(AndroidSchedulers.mainThread())
.doOnNext(NotificationConsumer())
.observeOn(Schedulers.io())
.buffer(BUFFER_COUNT_BEFORE_INSERT)
.doOnNext(DatabaseConsumer())
.subscribeOn(Schedulers.io())
.toList()
.flatMap { x -> postProcessFeed().toSingleDefault(x.flatten()) }
}
postProcessFeed()
return notifications
}
fun cancel() {
@ -243,79 +285,29 @@ class FeedLoadManager(private val context: Context) {
* Remove streams from the feed which are older than [FeedDatabaseManager.FEED_OLDEST_ALLOWED_DATE].
* Remove streams from the database which are not linked / used by any table.
*/
private fun postProcessFeed() = Completable.fromRunnable {
FeedEventManager.postEvent(FeedEventManager.Event.ProgressEvent(R.string.feed_processing_message))
feedDatabaseManager.removeOrphansOrOlderStreams()
private suspend fun postProcessFeed() {
withContext(Dispatchers.IO) {
FeedEventManager.postEvent(FeedEventManager.Event.ProgressEvent(R.string.feed_processing_message))
feedDatabaseManager.removeOrphansOrOlderStreams()
FeedEventManager.postEvent(FeedEventManager.Event.SuccessResultEvent(feedResultsHolder.itemsErrors))
}
FeedEventManager.postEvent(FeedEventManager.Event.SuccessResultEvent(feedResultsHolder.itemsErrors))
}.doOnSubscribe {
currentProgress.set(-1)
maxProgress.set(-1)
notificationUpdater.onNext(context.getString(R.string.feed_processing_message))
FeedEventManager.postEvent(FeedEventManager.Event.ProgressEvent(R.string.feed_processing_message))
}.subscribeOn(Schedulers.io())
private inner class NotificationConsumer : Consumer<Notification<FeedUpdateInfo>> {
override fun accept(item: Notification<FeedUpdateInfo>) {
currentProgress.incrementAndGet()
notificationUpdater.onNext(item.value?.name.orEmpty())
broadcastProgress()
}
}
private inner class DatabaseConsumer : Consumer<List<Notification<FeedUpdateInfo>>> {
override fun accept(list: List<Notification<FeedUpdateInfo>>) {
feedDatabaseManager.database().runInTransaction {
for (notification in list) {
when {
notification.isOnNext -> {
val info = notification.value!!
notification.value!!.newStreams = filterNewStreams(info.streams)
feedDatabaseManager.upsertAll(info.uid, info.streams)
subscriptionManager.updateFromInfo(info)
if (info.errors.isNotEmpty()) {
feedResultsHolder.addErrors(
info.errors.map {
FeedLoadService.RequestException(
info.uid,
"${info.serviceId}:${info.url}",
it
)
}
)
feedDatabaseManager.markAsOutdated(info.uid)
}
}
notification.isOnError -> {
val error = notification.error
feedResultsHolder.addError(error!!)
if (error is FeedLoadService.RequestException) {
feedDatabaseManager.markAsOutdated(error.subscriptionId)
}
}
}
}
}
}
private fun filterNewStreams(list: List<StreamInfoItem>): List<StreamInfoItem> {
return list.filter {
!feedDatabaseManager.doesStreamExist(it) &&
it.uploadDate != null &&
// Streams older than this date are automatically removed from the feed.
// Therefore, streams which are not in the database,
// but older than this date, are considered old.
it.uploadDate!!.offsetDateTime().isAfter(
FeedDatabaseManager.FEED_OLDEST_ALLOWED_DATE
)
}
private suspend fun filterNewStreams(list: List<StreamInfoItem>): List<StreamInfoItem> {
return list.filter {
!feedDatabaseManager.doesStreamExist(it) &&
it.uploadDate != null &&
// Streams older than this date are automatically removed from the feed.
// Therefore, streams which are not in the database,
// but older than this date, are considered old.
it.uploadDate!!.offsetDateTime() > FeedDatabaseManager.FEED_OLDEST_ALLOWED_DATE
}
}

View File

@ -19,22 +19,24 @@
package org.schabi.newpipe.local.feed.service
import android.app.Service
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Build
import android.os.IBinder
import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.app.PendingIntentCompat
import androidx.core.app.ServiceCompat
import androidx.lifecycle.LifecycleService
import androidx.lifecycle.lifecycleScope
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.disposables.Disposable
import io.reactivex.rxjava3.functions.Function
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.launch
import org.schabi.newpipe.App
import org.schabi.newpipe.MainActivity.DEBUG
import org.schabi.newpipe.R
@ -43,7 +45,7 @@ import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.ErrorResultE
import org.schabi.newpipe.local.feed.service.FeedEventManager.postEvent
import java.util.concurrent.TimeUnit
class FeedLoadService : Service() {
class FeedLoadService : LifecycleService() {
companion object {
private val TAG = FeedLoadService::class.java.simpleName
const val NOTIFICATION_ID = 7293450
@ -72,35 +74,34 @@ class FeedLoadService : Service() {
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
if (DEBUG) {
Log.d(
TAG,
"onStartCommand() called with: intent = [" + intent + "]," +
" flags = [" + flags + "], startId = [" + startId + "]"
"onStartCommand() called with: intent = [$intent], flags = [$flags], " +
"startId = [$startId]"
)
}
if (intent == null || loadingDisposable != null) {
if (intent == null) {
return START_NOT_STICKY
}
setupNotification()
setupBroadcastReceiver()
val groupId = intent.getLongExtra(EXTRA_GROUP_ID, FeedGroupEntity.GROUP_ALL_ID)
loadingDisposable = feedLoadManager.startLoading(groupId)
.observeOn(AndroidSchedulers.mainThread())
.doOnSubscribe {
startForeground(NOTIFICATION_ID, notificationBuilder.build())
}
.subscribe { _, error: Throwable? -> // explicitly mark error as nullable
if (error != null) {
Log.e(TAG, "Error while storing result", error)
handleError(error)
return@subscribe
}
stopService()
lifecycleScope.launch(
CoroutineExceptionHandler { _, throwable ->
Log.e(TAG, "Error while storing result", throwable)
handleError(throwable)
}
) {
val groupId = intent.getLongExtra(EXTRA_GROUP_ID, FeedGroupEntity.GROUP_ALL_ID)
startForeground(NOTIFICATION_ID, notificationBuilder.build())
feedLoadManager.startLoading(groupId)
stopService()
}
return START_NOT_STICKY
}
@ -116,10 +117,6 @@ class FeedLoadService : Service() {
stopSelf()
}
override fun onBind(intent: Intent): IBinder? {
return null
}
// /////////////////////////////////////////////////////////////////////////
// Loading & Handling
// /////////////////////////////////////////////////////////////////////////

View File

@ -312,7 +312,6 @@ public class HistoryRecordManager {
///////////////////////////////////////////////////////
public Single<Integer> removeOrphanedRecords() {
return Single.fromCallable(streamTable::deleteOrphans).subscribeOn(Schedulers.io());
return Single.fromCallable(streamTable::deleteOrphansBlocking).subscribeOn(Schedulers.io());
}
}

View File

@ -8,8 +8,8 @@ import org.schabi.newpipe.database.playlist.PlaylistDuplicatesEntry;
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
import org.schabi.newpipe.local.LocalItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.image.CoilHelper;
import java.time.format.DateTimeFormatter;
@ -30,17 +30,16 @@ public class LocalPlaylistItemHolder extends PlaylistItemHolder {
public void updateFromItem(final LocalItem localItem,
final HistoryRecordManager historyRecordManager,
final DateTimeFormatter dateTimeFormatter) {
if (!(localItem instanceof PlaylistMetadataEntry)) {
if (!(localItem instanceof PlaylistMetadataEntry item)) {
return;
}
final PlaylistMetadataEntry item = (PlaylistMetadataEntry) localItem;
itemTitleView.setText(item.name);
itemStreamCountView.setText(Localization.localizeStreamCountMini(
itemStreamCountView.getContext(), item.streamCount));
itemUploaderView.setVisibility(View.INVISIBLE);
PicassoHelper.loadPlaylistThumbnail(item.thumbnailUrl).into(itemThumbnailView);
CoilHelper.INSTANCE.loadPlaylistThumbnail(itemThumbnailView, item.thumbnailUrl);
if (item instanceof PlaylistDuplicatesEntry
&& ((PlaylistDuplicatesEntry) item).timesStreamIsContained > 0) {

View File

@ -16,8 +16,8 @@ import org.schabi.newpipe.local.LocalItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.DependentPreferenceHelper;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.ServiceHelper;
import org.schabi.newpipe.util.image.CoilHelper;
import org.schabi.newpipe.views.AnimatedProgressBar;
import java.time.format.DateTimeFormatter;
@ -83,8 +83,8 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder {
}
// Default thumbnail is shown on error, while loading and if the url is empty
PicassoHelper.loadThumbnail(item.getStreamEntity().getThumbnailUrl())
.into(itemThumbnailView);
CoilHelper.INSTANCE.loadThumbnail(itemThumbnailView,
item.getStreamEntity().getThumbnailUrl());
itemView.setOnClickListener(view -> {
if (itemBuilder.getOnItemSelectedListener() != null) {

View File

@ -16,8 +16,8 @@ import org.schabi.newpipe.local.LocalItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.DependentPreferenceHelper;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.ServiceHelper;
import org.schabi.newpipe.util.image.CoilHelper;
import org.schabi.newpipe.views.AnimatedProgressBar;
import java.time.format.DateTimeFormatter;
@ -117,8 +117,8 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder {
}
// Default thumbnail is shown on error, while loading and if the url is empty
PicassoHelper.loadThumbnail(item.getStreamEntity().getThumbnailUrl())
.into(itemThumbnailView);
CoilHelper.INSTANCE.loadThumbnail(itemThumbnailView,
item.getStreamEntity().getThumbnailUrl());
itemView.setOnClickListener(view -> {
if (itemBuilder.getOnItemSelectedListener() != null) {

View File

@ -8,8 +8,8 @@ import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
import org.schabi.newpipe.local.LocalItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.ServiceHelper;
import org.schabi.newpipe.util.image.CoilHelper;
import java.time.format.DateTimeFormatter;
@ -29,10 +29,9 @@ public class RemotePlaylistItemHolder extends PlaylistItemHolder {
public void updateFromItem(final LocalItem localItem,
final HistoryRecordManager historyRecordManager,
final DateTimeFormatter dateTimeFormatter) {
if (!(localItem instanceof PlaylistRemoteEntity)) {
if (!(localItem instanceof PlaylistRemoteEntity item)) {
return;
}
final PlaylistRemoteEntity item = (PlaylistRemoteEntity) localItem;
itemTitleView.setText(item.getName());
itemStreamCountView.setText(Localization.localizeStreamCountMini(
@ -45,7 +44,7 @@ public class RemotePlaylistItemHolder extends PlaylistItemHolder {
itemUploaderView.setText(ServiceHelper.getNameOfServiceById(item.getServiceId()));
}
PicassoHelper.loadPlaylistThumbnail(item.getThumbnailUrl()).into(itemThumbnailView);
CoilHelper.INSTANCE.loadPlaylistThumbnail(itemThumbnailView, item.getThumbnailUrl());
super.updateFromItem(localItem, historyRecordManager, dateTimeFormatter);
}

View File

@ -2,10 +2,12 @@ package org.schabi.newpipe.local.subscription
import android.content.Context
import android.util.Pair
import androidx.room.withTransaction
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Completable
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.schedulers.Schedulers
import kotlinx.coroutines.runBlocking
import org.schabi.newpipe.NewPipeDatabase
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.database.stream.model.StreamEntity
@ -26,7 +28,7 @@ class SubscriptionManager(context: Context) {
private val feedDatabaseManager = FeedDatabaseManager(context)
fun subscriptionTable(): SubscriptionDAO = subscriptionTable
fun subscriptions() = subscriptionTable.all
fun subscriptions() = subscriptionTable.getAll()
fun getSubscriptions(
currentGroupId: Long = FeedGroupEntity.GROUP_ALL_ID,
@ -44,16 +46,18 @@ class SubscriptionManager(context: Context) {
}
}
showOnlyUngrouped -> subscriptionTable.getSubscriptionsOnlyUngrouped(currentGroupId)
else -> subscriptionTable.all
else -> subscriptionTable.getAll()
}
}
fun upsertAll(infoList: List<Pair<ChannelInfo, List<ChannelTabInfo>>>): List<SubscriptionEntity> {
fun upsertAll(
infoList: List<Pair<ChannelInfo, List<ChannelTabInfo>>>
): List<SubscriptionEntity> = runBlocking {
val listEntities = subscriptionTable.upsertAll(
infoList.map { SubscriptionEntity.from(it.first) }
)
database.runInTransaction {
database.withTransaction {
infoList.forEachIndexed { index, info ->
info.second.forEach {
feedDatabaseManager.upsertAll(
@ -64,7 +68,7 @@ class SubscriptionManager(context: Context) {
}
}
return listEntities
listEntities
}
fun updateChannelInfo(info: ChannelInfo): Completable =
@ -96,7 +100,7 @@ class SubscriptionManager(context: Context) {
}
}
fun updateFromInfo(info: FeedUpdateInfo) {
suspend fun updateFromInfo(info: FeedUpdateInfo) {
val subscriptionEntity = subscriptionTable.getSubscription(info.uid)
subscriptionEntity.name = info.name
@ -108,7 +112,7 @@ class SubscriptionManager(context: Context) {
info.description?.let { subscriptionEntity.description = it }
info.subscriberCount?.let { subscriptionEntity.subscriberCount = it }
subscriptionTable.update(subscriptionEntity)
subscriptionTable.updateSubscription(subscriptionEntity)
}
fun deleteSubscription(serviceId: Int, url: String): Completable {

View File

@ -9,7 +9,7 @@ import org.schabi.newpipe.R
import org.schabi.newpipe.extractor.channel.ChannelInfoItem
import org.schabi.newpipe.util.Localization
import org.schabi.newpipe.util.OnClickGesture
import org.schabi.newpipe.util.image.PicassoHelper
import org.schabi.newpipe.util.image.CoilHelper
class ChannelItem(
private val infoItem: ChannelInfoItem,
@ -39,7 +39,7 @@ class ChannelItem(
itemChannelDescriptionView.text = infoItem.description
}
PicassoHelper.loadAvatar(infoItem.thumbnails).into(itemThumbnailView)
CoilHelper.loadAvatar(itemThumbnailView, infoItem.thumbnails)
gesturesListener?.run {
viewHolder.root.setOnClickListener { selected(infoItem) }

View File

@ -10,7 +10,7 @@ import org.schabi.newpipe.database.subscription.SubscriptionEntity
import org.schabi.newpipe.databinding.PickerSubscriptionItemBinding
import org.schabi.newpipe.ktx.AnimationType
import org.schabi.newpipe.ktx.animate
import org.schabi.newpipe.util.image.PicassoHelper
import org.schabi.newpipe.util.image.CoilHelper
data class PickerSubscriptionItem(
val subscriptionEntity: SubscriptionEntity,
@ -21,7 +21,7 @@ data class PickerSubscriptionItem(
override fun getSpanSize(spanCount: Int, position: Int): Int = 1
override fun bind(viewBinding: PickerSubscriptionItemBinding, position: Int) {
PicassoHelper.loadAvatar(subscriptionEntity.avatarUrl).into(viewBinding.thumbnailView)
CoilHelper.loadAvatar(viewBinding.thumbnailView, subscriptionEntity.avatarUrl)
viewBinding.titleView.text = subscriptionEntity.name
viewBinding.selectedHighlight.isVisible = isSelected
}

View File

@ -60,6 +60,7 @@ import android.view.LayoutInflater;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.graphics.drawable.DrawableKt;
import androidx.core.math.MathUtils;
import androidx.preference.PreferenceManager;
@ -77,8 +78,6 @@ import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.MappingTrackSelector;
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
import com.google.android.exoplayer2.video.VideoSize;
import com.squareup.picasso.Picasso;
import com.squareup.picasso.Target;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R;
@ -86,8 +85,8 @@ import org.schabi.newpipe.databinding.PlayerBinding;
import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.ErrorUtil;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.stream.AudioStream;
import org.schabi.newpipe.extractor.Image;
import org.schabi.newpipe.extractor.stream.AudioStream;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.extractor.stream.VideoStream;
@ -118,14 +117,15 @@ import org.schabi.newpipe.player.ui.VideoPlayerUi;
import org.schabi.newpipe.util.DependentPreferenceHelper;
import org.schabi.newpipe.util.ListHelper;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.SerializedCache;
import org.schabi.newpipe.util.StreamTypeUtil;
import org.schabi.newpipe.util.image.CoilHelper;
import java.util.List;
import java.util.Optional;
import java.util.stream.IntStream;
import coil.target.Target;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
@ -174,7 +174,6 @@ public final class Player implements PlaybackListener, Listener {
//////////////////////////////////////////////////////////////////////////*/
public static final int RENDERER_UNAVAILABLE = -1;
private static final String PICASSO_PLAYER_THUMBNAIL_TAG = "PICASSO_PLAYER_THUMBNAIL_TAG";
/*//////////////////////////////////////////////////////////////////////////
// Playback
@ -246,12 +245,6 @@ public final class Player implements PlaybackListener, Listener {
@NonNull
private final CompositeDisposable databaseUpdateDisposable = new CompositeDisposable();
// This is the only listener we need for thumbnail loading, since there is always at most only
// one thumbnail being loaded at a time. This field is also here to maintain a strong reference,
// which would otherwise be garbage collected since Picasso holds weak references to targets.
@NonNull
private final Target currentThumbnailTarget;
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
@ -295,8 +288,6 @@ public final class Player implements PlaybackListener, Listener {
videoResolver = new VideoPlaybackResolver(context, dataSource, getQualityResolver());
audioResolver = new AudioPlaybackResolver(context, dataSource);
currentThumbnailTarget = getCurrentThumbnailTarget();
// The UIs added here should always be present. They will be initialized when the player
// reaches the initialization step. Make sure the media session ui is before the
// notification ui in the UIs list, since the notification depends on the media session in
@ -602,7 +593,6 @@ public final class Player implements PlaybackListener, Listener {
databaseUpdateDisposable.clear();
progressUpdateDisposable.set(null);
cancelLoadingCurrentThumbnail();
UIs.destroyAll(Object.class); // destroy every UI: obviously every UI extends Object
}
@ -776,67 +766,52 @@ public final class Player implements PlaybackListener, Listener {
//////////////////////////////////////////////////////////////////////////*/
//region Thumbnail loading
private Target getCurrentThumbnailTarget() {
// a Picasso target is just a listener for thumbnail loading events
return new Target() {
@Override
public void onBitmapLoaded(final Bitmap bitmap, final Picasso.LoadedFrom from) {
if (DEBUG) {
Log.d(TAG, "Thumbnail - onBitmapLoaded() called with: bitmap = [" + bitmap
+ " -> " + bitmap.getWidth() + "x" + bitmap.getHeight() + "], from = ["
+ from + "]");
}
// there is a new thumbnail, so e.g. the end screen thumbnail needs to change, too.
onThumbnailLoaded(bitmap);
}
@Override
public void onBitmapFailed(final Exception e, final Drawable errorDrawable) {
Log.e(TAG, "Thumbnail - onBitmapFailed() called", e);
// there is a new thumbnail, so e.g. the end screen thumbnail needs to change, too.
onThumbnailLoaded(null);
}
@Override
public void onPrepareLoad(final Drawable placeHolderDrawable) {
if (DEBUG) {
Log.d(TAG, "Thumbnail - onPrepareLoad() called");
}
}
};
}
private void loadCurrentThumbnail(final List<Image> thumbnails) {
if (DEBUG) {
Log.d(TAG, "Thumbnail - loadCurrentThumbnail() called with thumbnails = ["
+ thumbnails.size() + "]");
}
// first cancel any previous loading
cancelLoadingCurrentThumbnail();
// Unset currentThumbnail, since it is now outdated. This ensures it is not used in media
// session metadata while the new thumbnail is being loaded by Picasso.
// session metadata while the new thumbnail is being loaded by Coil.
onThumbnailLoaded(null);
if (thumbnails.isEmpty()) {
return;
}
// scale down the notification thumbnail for performance
PicassoHelper.loadScaledDownThumbnail(context, thumbnails)
.tag(PICASSO_PLAYER_THUMBNAIL_TAG)
.into(currentThumbnailTarget);
}
final var target = new Target() {
@Override
public void onError(@Nullable final Drawable error) {
Log.e(TAG, "Thumbnail - onError() called");
// there is a new thumbnail, so e.g. the end screen thumbnail needs to change, too.
onThumbnailLoaded(null);
}
private void cancelLoadingCurrentThumbnail() {
// cancel the Picasso job associated with the player thumbnail, if any
PicassoHelper.cancelTag(PICASSO_PLAYER_THUMBNAIL_TAG);
@Override
public void onStart(@Nullable final Drawable placeholder) {
if (DEBUG) {
Log.d(TAG, "Thumbnail - onStart() called");
}
}
@Override
public void onSuccess(@NonNull final Drawable result) {
if (DEBUG) {
Log.d(TAG, "Thumbnail - onSuccess() called with: drawable = [" + result + "]");
}
// there is a new thumbnail, so e.g. the end screen thumbnail needs to change, too.
onThumbnailLoaded(DrawableKt.toBitmapOrNull(result, result.getIntrinsicWidth(),
result.getIntrinsicHeight(), null));
}
};
CoilHelper.INSTANCE.loadScaledDownThumbnail(context, thumbnails, target);
}
private void onThumbnailLoaded(@Nullable final Bitmap bitmap) {
// Avoid useless thumbnail updates, if the thumbnail has not actually changed. Based on the
// thumbnail loading code, this if would be skipped only when both bitmaps are `null`, since
// onThumbnailLoaded won't be called twice with the same nonnull bitmap by Picasso's target.
// onThumbnailLoaded won't be called twice with the same nonnull bitmap by Coil's target.
if (currentThumbnail != bitmap) {
currentThumbnail = bitmap;
UIs.call(playerUi -> playerUi.onThumbnailLoaded(bitmap));

View File

@ -6,8 +6,8 @@ import android.view.MotionEvent;
import android.view.View;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.ServiceHelper;
import org.schabi.newpipe.util.image.CoilHelper;
public class PlayQueueItemBuilder {
private static final String TAG = PlayQueueItemBuilder.class.toString();
@ -33,7 +33,7 @@ public class PlayQueueItemBuilder {
holder.itemDurationView.setVisibility(View.GONE);
}
PicassoHelper.loadThumbnail(item.getThumbnails()).into(holder.itemThumbnailView);
CoilHelper.INSTANCE.loadThumbnail(holder.itemThumbnailView, item.getThumbnails());
holder.itemRoot.setOnClickListener(view -> {
if (onItemClickListener != null) {

View File

@ -13,8 +13,9 @@ import androidx.collection.SparseArrayCompat;
import com.google.common.base.Stopwatch;
import org.schabi.newpipe.App;
import org.schabi.newpipe.extractor.stream.Frameset;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.image.CoilHelper;
import java.util.Comparator;
import java.util.List;
@ -177,8 +178,8 @@ public class SeekbarPreviewThumbnailHolder {
Log.d(TAG, "Downloading bitmap for seekbarPreview from '" + url + "'");
// Gets the bitmap within the timeout of 15 seconds imposed by default by OkHttpClient
// Ensure that your are not running on the main-Thread this will otherwise hang
final Bitmap bitmap = PicassoHelper.loadSeekbarThumbnailPreview(url).get();
// Ensure that you are not running on the main thread, otherwise this will hang
final var bitmap = CoilHelper.INSTANCE.loadBitmapBlocking(App.getApp(), url);
if (sw != null) {
Log.d(TAG, "Download of bitmap for seekbarPreview from '" + url + "' took "

View File

@ -13,10 +13,9 @@ import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.localization.ContentCountry;
import org.schabi.newpipe.extractor.localization.Localization;
import org.schabi.newpipe.util.image.ImageStrategy;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.image.PreferredImageQuality;
import java.io.IOException;
import coil.Coil;
public class ContentSettingsFragment extends BasePreferenceFragment {
private String youtubeRestrictedModeEnabledKey;
@ -42,14 +41,17 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
(preference, newValue) -> {
ImageStrategy.setPreferredImageQuality(PreferredImageQuality
.fromPreferenceKey(requireContext(), (String) newValue));
try {
PicassoHelper.clearCache(preference.getContext());
Toast.makeText(preference.getContext(),
R.string.thumbnail_cache_wipe_complete_notice, Toast.LENGTH_SHORT)
.show();
} catch (final IOException e) {
Log.e(TAG, "Unable to clear Picasso cache", e);
final var loader = Coil.imageLoader(preference.getContext());
if (loader.getMemoryCache() != null) {
loader.getMemoryCache().clear();
}
if (loader.getDiskCache() != null) {
loader.getDiskCache().clear();
}
Toast.makeText(preference.getContext(),
R.string.thumbnail_cache_wipe_complete_notice, Toast.LENGTH_SHORT)
.show();
return true;
});
}

View File

@ -10,7 +10,6 @@ import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.ErrorUtil;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.local.feed.notifications.NotificationWorker;
import org.schabi.newpipe.util.image.PicassoHelper;
import java.util.Optional;
@ -25,8 +24,6 @@ public class DebugSettingsFragment extends BasePreferenceFragment {
findPreference(getString(R.string.allow_heap_dumping_key));
final Preference showMemoryLeaksPreference =
findPreference(getString(R.string.show_memory_leaks_key));
final Preference showImageIndicatorsPreference =
findPreference(getString(R.string.show_image_indicators_key));
final Preference checkNewStreamsPreference =
findPreference(getString(R.string.check_new_streams_key));
final Preference crashTheAppPreference =
@ -38,7 +35,6 @@ public class DebugSettingsFragment extends BasePreferenceFragment {
assert allowHeapDumpingPreference != null;
assert showMemoryLeaksPreference != null;
assert showImageIndicatorsPreference != null;
assert checkNewStreamsPreference != null;
assert crashTheAppPreference != null;
assert showErrorSnackbarPreference != null;
@ -61,11 +57,6 @@ public class DebugSettingsFragment extends BasePreferenceFragment {
showMemoryLeaksPreference.setSummary(R.string.leak_canary_not_available);
}
showImageIndicatorsPreference.setOnPreferenceChangeListener((preference, newValue) -> {
PicassoHelper.setIndicatorsEnabled((Boolean) newValue);
return true;
});
checkNewStreamsPreference.setOnPreferenceClickListener(preference -> {
NotificationWorker.runNow(preference.getContext());
return true;

View File

@ -19,8 +19,8 @@ import org.schabi.newpipe.R;
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
import org.schabi.newpipe.error.ErrorUtil;
import org.schabi.newpipe.local.subscription.SubscriptionManager;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.ThemeHelper;
import org.schabi.newpipe.util.image.CoilHelper;
import java.util.List;
import java.util.Vector;
@ -190,7 +190,7 @@ public class SelectChannelFragment extends DialogFragment {
final SubscriptionEntity entry = subscriptions.get(position);
holder.titleView.setText(entry.getName());
holder.view.setOnClickListener(view -> clickedItem(position));
PicassoHelper.loadAvatar(entry.getAvatarUrl()).into(holder.thumbnailView);
CoilHelper.INSTANCE.loadAvatar(holder.thumbnailView, entry.getAvatarUrl());
}
@Override

View File

@ -27,7 +27,7 @@ import org.schabi.newpipe.error.ErrorUtil;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.image.CoilHelper;
import java.util.List;
import java.util.Vector;
@ -154,20 +154,15 @@ public class SelectPlaylistFragment extends DialogFragment {
final int position) {
final PlaylistLocalItem selectedItem = playlists.get(position);
if (selectedItem instanceof PlaylistMetadataEntry) {
final PlaylistMetadataEntry entry = ((PlaylistMetadataEntry) selectedItem);
if (selectedItem instanceof PlaylistMetadataEntry entry) {
holder.titleView.setText(entry.name);
holder.view.setOnClickListener(view -> clickedItem(position));
PicassoHelper.loadPlaylistThumbnail(entry.thumbnailUrl).into(holder.thumbnailView);
} else if (selectedItem instanceof PlaylistRemoteEntity) {
final PlaylistRemoteEntity entry = ((PlaylistRemoteEntity) selectedItem);
CoilHelper.INSTANCE.loadPlaylistThumbnail(holder.thumbnailView, entry.thumbnailUrl);
} else if (selectedItem instanceof PlaylistRemoteEntity entry) {
holder.titleView.setText(entry.getName());
holder.view.setOnClickListener(view -> clickedItem(position));
PicassoHelper.loadPlaylistThumbnail(entry.getThumbnailUrl())
.into(holder.thumbnailView);
CoilHelper.INSTANCE.loadPlaylistThumbnail(holder.thumbnailView,
entry.getThumbnailUrl());
}
}

View File

@ -24,8 +24,8 @@ import androidx.core.content.FileProvider;
import org.schabi.newpipe.BuildConfig;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.Image;
import org.schabi.newpipe.util.image.CoilHelper;
import org.schabi.newpipe.util.image.ImageStrategy;
import org.schabi.newpipe.util.image.PicassoHelper;
import java.io.File;
import java.io.FileOutputStream;
@ -278,7 +278,7 @@ public final class ShareUtils {
* @param content the content to share
* @param images a set of possible {@link Image}s of the subject, among which to choose with
* {@link ImageStrategy#choosePreferredImage(List)} since that's likely to
* provide an image that is in Picasso's cache
* provide an image that is in Coil's cache
*/
public static void shareText(@NonNull final Context context,
@NonNull final String title,
@ -335,15 +335,7 @@ public final class ShareUtils {
}
/**
* Generate a {@link ClipData} with the image of the content shared, if it's in the app cache.
*
* <p>
* In order not to worry about network issues (timeouts, DNS issues, low connection speed, ...)
* when sharing a content, only images in the {@link com.squareup.picasso.LruCache LruCache}
* used by the Picasso library inside {@link PicassoHelper} are used as preview images. If the
* thumbnail image is not in the cache, no {@link ClipData} will be generated and {@code null}
* will be returned.
* </p>
* Generate a {@link ClipData} with the image of the content shared.
*
* <p>
* In order to display the image in the content preview of the Android share sheet, an URI of
@ -359,9 +351,8 @@ public final class ShareUtils {
* </p>
*
* <p>
* This method will call {@link PicassoHelper#getImageFromCacheIfPresent(String)} to get the
* thumbnail of the content in the {@link com.squareup.picasso.LruCache LruCache} used by
* the Picasso library inside {@link PicassoHelper}.
* This method will call {@link CoilHelper#loadBitmap(Context, String)} to get the
* thumbnail of the content.
* </p>
*
* <p>
@ -378,7 +369,7 @@ public final class ShareUtils {
@NonNull final Context context,
@NonNull final String thumbnailUrl) {
try {
final Bitmap bitmap = PicassoHelper.getImageFromCacheIfPresent(thumbnailUrl);
final var bitmap = CoilHelper.INSTANCE.loadBitmapBlocking(context, thumbnailUrl);
if (bitmap == null) {
return null;
}

View File

@ -0,0 +1,148 @@
package org.schabi.newpipe.util.image
import android.content.Context
import android.graphics.Bitmap
import android.util.Log
import android.widget.ImageView
import androidx.annotation.DrawableRes
import androidx.core.graphics.drawable.toBitmapOrNull
import coil.imageLoader
import coil.request.ImageRequest
import coil.size.Size
import coil.target.Target
import coil.transform.Transformation
import kotlinx.coroutines.runBlocking
import org.schabi.newpipe.MainActivity
import org.schabi.newpipe.R
import org.schabi.newpipe.extractor.Image
import org.schabi.newpipe.ktx.scale
import kotlin.math.min
object CoilHelper {
private val TAG = CoilHelper::class.java.simpleName
fun loadBitmapBlocking(context: Context, url: String?) = runBlocking {
loadBitmap(context, url, 0)
}
suspend fun loadBitmap(
context: Context,
url: String?,
@DrawableRes placeholderResId: Int
): Bitmap? {
val request = getImageRequest(context, url, placeholderResId).build()
return context.imageLoader.execute(request).drawable?.toBitmapOrNull()
}
fun loadAvatar(target: ImageView, images: List<Image>) {
loadImageDefault(target, images, R.drawable.placeholder_person)
}
fun loadAvatar(target: ImageView, url: String?) {
loadImageDefault(target, url, R.drawable.placeholder_person)
}
fun loadThumbnail(target: ImageView, images: List<Image>) {
loadImageDefault(target, images, R.drawable.placeholder_thumbnail_video)
}
fun loadThumbnail(target: ImageView, url: String?) {
loadImageDefault(target, url, R.drawable.placeholder_thumbnail_video)
}
fun loadScaledDownThumbnail(context: Context, images: List<Image>, target: Target) {
val url = ImageStrategy.choosePreferredImage(images)
val request = getImageRequest(context, url, R.drawable.placeholder_thumbnail_video)
.target(target)
.transformations(object : Transformation {
override val cacheKey = "COIL_PLAYER_THUMBNAIL_TRANSFORMATION_KEY"
override suspend fun transform(input: Bitmap, size: Size): Bitmap {
if (MainActivity.DEBUG) {
Log.d(TAG, "Thumbnail - transform() called")
}
val notificationThumbnailWidth = min(
context.resources.getDimension(R.dimen.player_notification_thumbnail_width),
input.width.toFloat()
).toInt()
var newHeight = input.height / (input.width / notificationThumbnailWidth)
val result = input.scale(notificationThumbnailWidth, newHeight)
if (result == input || !result.isMutable) {
// create a new mutable bitmap to prevent strange crashes on some
// devices (see #4638)
newHeight = input.height / (input.width / (notificationThumbnailWidth - 1))
val copied = input.scale(notificationThumbnailWidth, newHeight)
input.recycle()
return copied
} else {
input.recycle()
return result
}
}
})
.build()
context.imageLoader.enqueue(request)
}
fun loadDetailsThumbnail(target: ImageView, images: List<Image>) {
val url = ImageStrategy.choosePreferredImage(images)
loadImageDefault(target, url, R.drawable.placeholder_thumbnail_video, false)
}
fun loadBanner(target: ImageView, images: List<Image>) {
loadImageDefault(target, images, R.drawable.placeholder_channel_banner)
}
fun loadPlaylistThumbnail(target: ImageView, images: List<Image>) {
loadImageDefault(target, images, R.drawable.placeholder_thumbnail_playlist)
}
fun loadPlaylistThumbnail(target: ImageView, url: String?) {
loadImageDefault(target, url, R.drawable.placeholder_thumbnail_playlist)
}
private fun loadImageDefault(
target: ImageView,
images: List<Image>,
@DrawableRes placeholderResId: Int
) {
loadImageDefault(target, ImageStrategy.choosePreferredImage(images), placeholderResId)
}
private fun loadImageDefault(
target: ImageView,
url: String?,
@DrawableRes placeholderResId: Int,
showPlaceholder: Boolean = true
) {
val request = getImageRequest(target.context, url, placeholderResId, showPlaceholder)
.target(target)
.build()
target.context.imageLoader.enqueue(request)
}
private fun getImageRequest(
context: Context,
url: String?,
@DrawableRes placeholderResId: Int,
showPlaceholderWhileLoading: Boolean = true
): ImageRequest.Builder {
// if the URL was chosen with `choosePreferredImage` it will be null, but check again
// `shouldLoadImages` in case the URL was chosen with `imageListToDbUrl` (which is the case
// for URLs stored in the database)
val takenUrl = url?.takeIf { it.isNotEmpty() && ImageStrategy.shouldLoadImages() }
return ImageRequest.Builder(context)
.data(takenUrl)
.error(placeholderResId)
.apply {
if (takenUrl != null || showPlaceholderWhileLoading) {
placeholder(placeholderResId)
}
}
}
}

View File

@ -1,224 +0,0 @@
package org.schabi.newpipe.util.image;
import static org.schabi.newpipe.MainActivity.DEBUG;
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
import static org.schabi.newpipe.util.image.ImageStrategy.choosePreferredImage;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Bitmap;
import android.util.Log;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.graphics.BitmapCompat;
import com.squareup.picasso.Cache;
import com.squareup.picasso.LruCache;
import com.squareup.picasso.OkHttp3Downloader;
import com.squareup.picasso.Picasso;
import com.squareup.picasso.RequestCreator;
import com.squareup.picasso.Transformation;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.Image;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.TimeUnit;
import okhttp3.OkHttpClient;
public final class PicassoHelper {
private static final String TAG = PicassoHelper.class.getSimpleName();
private static final String PLAYER_THUMBNAIL_TRANSFORMATION_KEY =
"PICASSO_PLAYER_THUMBNAIL_TRANSFORMATION_KEY";
private PicassoHelper() {
}
private static Cache picassoCache;
private static OkHttpClient picassoDownloaderClient;
// suppress because terminate() is called in App.onTerminate(), preventing leaks
@SuppressLint("StaticFieldLeak")
private static Picasso picassoInstance;
public static void init(final Context context) {
picassoCache = new LruCache(10 * 1024 * 1024);
picassoDownloaderClient = new OkHttpClient.Builder()
.cache(new okhttp3.Cache(new File(context.getExternalCacheDir(), "picasso"),
50L * 1024L * 1024L))
// this should already be the default timeout in OkHttp3, but just to be sure...
.callTimeout(15, TimeUnit.SECONDS)
.build();
picassoInstance = new Picasso.Builder(context)
.memoryCache(picassoCache) // memory cache
.downloader(new OkHttp3Downloader(picassoDownloaderClient)) // disk cache
.defaultBitmapConfig(Bitmap.Config.RGB_565)
.build();
}
public static void terminate() {
picassoCache = null;
picassoDownloaderClient = null;
if (picassoInstance != null) {
picassoInstance.shutdown();
picassoInstance = null;
}
}
public static void clearCache(final Context context) throws IOException {
picassoInstance.shutdown();
picassoCache.clear(); // clear memory cache
final okhttp3.Cache diskCache = picassoDownloaderClient.cache();
if (diskCache != null) {
diskCache.delete(); // clear disk cache
}
init(context);
}
public static void cancelTag(final Object tag) {
picassoInstance.cancelTag(tag);
}
public static void setIndicatorsEnabled(final boolean enabled) {
picassoInstance.setIndicatorsEnabled(enabled); // useful for debugging
}
public static RequestCreator loadAvatar(@NonNull final List<Image> images) {
return loadImageDefault(images, R.drawable.placeholder_person);
}
public static RequestCreator loadAvatar(@Nullable final String url) {
return loadImageDefault(url, R.drawable.placeholder_person);
}
public static RequestCreator loadThumbnail(@NonNull final List<Image> images) {
return loadImageDefault(images, R.drawable.placeholder_thumbnail_video);
}
public static RequestCreator loadThumbnail(@Nullable final String url) {
return loadImageDefault(url, R.drawable.placeholder_thumbnail_video);
}
public static RequestCreator loadDetailsThumbnail(@NonNull final List<Image> images) {
return loadImageDefault(choosePreferredImage(images),
R.drawable.placeholder_thumbnail_video, false);
}
public static RequestCreator loadBanner(@NonNull final List<Image> images) {
return loadImageDefault(images, R.drawable.placeholder_channel_banner);
}
public static RequestCreator loadPlaylistThumbnail(@NonNull final List<Image> images) {
return loadImageDefault(images, R.drawable.placeholder_thumbnail_playlist);
}
public static RequestCreator loadPlaylistThumbnail(@Nullable final String url) {
return loadImageDefault(url, R.drawable.placeholder_thumbnail_playlist);
}
public static RequestCreator loadSeekbarThumbnailPreview(@Nullable final String url) {
return picassoInstance.load(url);
}
public static RequestCreator loadNotificationIcon(@Nullable final String url) {
return loadImageDefault(url, R.drawable.ic_newpipe_triangle_white);
}
public static RequestCreator loadScaledDownThumbnail(final Context context,
@NonNull final List<Image> images) {
// scale down the notification thumbnail for performance
return PicassoHelper.loadThumbnail(images)
.transform(new Transformation() {
@Override
public Bitmap transform(final Bitmap source) {
if (DEBUG) {
Log.d(TAG, "Thumbnail - transform() called");
}
final float notificationThumbnailWidth = Math.min(
context.getResources()
.getDimension(R.dimen.player_notification_thumbnail_width),
source.getWidth());
final Bitmap result = BitmapCompat.createScaledBitmap(
source,
(int) notificationThumbnailWidth,
(int) (source.getHeight()
/ (source.getWidth() / notificationThumbnailWidth)),
null,
true);
if (result == source || !result.isMutable()) {
// create a new mutable bitmap to prevent strange crashes on some
// devices (see #4638)
final Bitmap copied = BitmapCompat.createScaledBitmap(
source,
(int) notificationThumbnailWidth - 1,
(int) (source.getHeight() / (source.getWidth()
/ (notificationThumbnailWidth - 1))),
null,
true);
source.recycle();
return copied;
} else {
source.recycle();
return result;
}
}
@Override
public String key() {
return PLAYER_THUMBNAIL_TRANSFORMATION_KEY;
}
});
}
@Nullable
public static Bitmap getImageFromCacheIfPresent(@NonNull final String imageUrl) {
// URLs in the internal cache finish with \n so we need to add \n to image URLs
return picassoCache.get(imageUrl + "\n");
}
private static RequestCreator loadImageDefault(@NonNull final List<Image> images,
@DrawableRes final int placeholderResId) {
return loadImageDefault(choosePreferredImage(images), placeholderResId);
}
private static RequestCreator loadImageDefault(@Nullable final String url,
@DrawableRes final int placeholderResId) {
return loadImageDefault(url, placeholderResId, true);
}
private static RequestCreator loadImageDefault(@Nullable final String url,
@DrawableRes final int placeholderResId,
final boolean showPlaceholderWhileLoading) {
// if the URL was chosen with `choosePreferredImage` it will be null, but check again
// `shouldLoadImages` in case the URL was chosen with `imageListToDbUrl` (which is the case
// for URLs stored in the database)
if (isNullOrEmpty(url) || !ImageStrategy.shouldLoadImages()) {
return picassoInstance
.load((String) null)
.placeholder(placeholderResId) // show placeholder when no image should load
.error(placeholderResId);
} else {
final RequestCreator requestCreator = picassoInstance
.load(url)
.error(placeholderResId);
if (showPlaceholderWhileLoading) {
requestCreator.placeholder(placeholderResId);
}
return requestCreator;
}
}
}

View File

@ -302,7 +302,6 @@
<item quantity="other">%s مشارك</item>
</plurals>
<string name="loading_metadata_title">جلب البيانات الوصفية…</string>
<string name="show_image_indicators_title">إظهار مؤشرات الصور</string>
<string name="app_update_available_notification_text">انقر للتنزيل %s</string>
<string name="feed_use_dedicated_fetch_method_disable_button">تعطيل الوضع السريع</string>
<string name="enumeration_comma">,</string>
@ -830,7 +829,6 @@
<string name="you_successfully_subscribed">لقد اشتركت الآن في هذه القناة</string>
<string name="downloads_storage_use_saf_summary_api_29">بدءًا من Android 10، يتم دعم \"Storage Access Framework\" فقط</string>
<string name="generate_unique_name">إنشاء اسم فريد</string>
<string name="show_image_indicators_summary">أظهر أشرطة ملونة لبيكاسو أعلى الصور تشير إلى مصدرها: الأحمر للشبكة والأزرق للقرص والأخضر للذاكرة</string>
<string name="error_ssl_exception">فشل الاتصال الآمن</string>
<string name="youtube_music_premium_content">يتوفر هذا الفيديو فقط لأعضاء YouTube Music Premium، لذلك لا يمكن بثه أو تنزيله من قبل NewPipe.</string>
<string name="previous_stream">البث السابق</string>

View File

@ -674,8 +674,6 @@
<string name="seekbar_preview_thumbnail_title">معاينة مصغرة على شريط التمرير</string>
<string name="mark_as_watched">وضع علامة على تمت مشاهدته</string>
<string name="detail_heart_img_view_description">أُعجب بها منشئ المحتوى</string>
<string name="show_image_indicators_summary">أظهر أشرطة ملونة لبيكاسو أعلى الصور تشير إلى مصدرها: الأحمر للشبكة والأزرق للقرص والأخضر للذاكرة</string>
<string name="show_image_indicators_title">إظهار مؤشرات الصور</string>
<string name="remote_search_suggestions">اقتراحات البحث عن بعد</string>
<string name="local_search_suggestions">اقتراحات البحث المحلية</string>
<string name="main_page_content_swipe_remove">اسحب العناصر لإزالتها</string>

View File

@ -505,8 +505,6 @@
<string name="preferred_player_fetcher_notification_title">Məlumat əldə edilir…</string>
<string name="show_original_time_ago_title">Elementlərdə orijinal əvvəlki vaxtı göstər</string>
<string name="enable_disposed_exceptions_title">Yaşam dövrəsi xaricindəki xətaları bildir</string>
<string name="show_image_indicators_title">Şəkil göstəricilərini göstər</string>
<string name="show_image_indicators_summary">Şəkillərin üzərində mənbəsini göstərən Picasso rəngli lentləri göstər: şəbəkə üçün qırmızı, disk üçün mavi və yaddaş üçün yaşıl</string>
<string name="pause_downloads_on_mobile_desc">Bəzi endirmələri dayandırmaq mümkün olmasa da, mobil dataya keçərkən faydalıdır</string>
<string name="close">Bağla</string>
<string name="error_progress_lost">Fayl silindiyi üçün irəliləyiş itirildi</string>

View File

@ -630,8 +630,6 @@
<string name="faq">Перайсці на вэбсайт</string>
<string name="main_page_content_swipe_remove">Правядзіце пальцам па элементах, каб выдаліць іх</string>
<string name="unset_playlist_thumbnail">Адмяніць пастаянную мініяцюру</string>
<string name="show_image_indicators_title">Паказаць індыкатары выявы</string>
<string name="show_image_indicators_summary">Паказваць каляровыя стужкі Пікаса на выявах, якія пазначаюць іх крыніцу: чырвоная для сеткі, сіняя для дыска і зялёная для памяці</string>
<string name="feed_processing_message">Апрацоўка стужкі…</string>
<string name="downloads_storage_ask_summary_no_saf_notice">Вам будзе прапанавана, дзе захоўваць кожную спампоўку</string>
<string name="feed_notification_loading">Загрузка стужкі…</string>

View File

@ -539,7 +539,6 @@
<string name="restricted_video">Това видео е с възрастова граница.
\n
\nВключете „%1$s“ в настройките ако искате да го пуснете.</string>
<string name="show_image_indicators_summary">Показвай цветни Picasso-панделки в горната част на изображенията като индикатор за техния произход (червен от мрежата, син от диска и червен от паметта)</string>
<string name="auto_device_theme_title">Автоматична (тази на устройството)</string>
<string name="notification_scale_to_square_image_summary">Мащабиране на миниатюрата в известието от 16:9 към 1:1 формат (възможни са изкривявания)</string>
<string name="select_a_playlist">Избете плейлист</string>

View File

@ -589,7 +589,6 @@
<string name="streams_notification_channel_name">নতুন ধারা</string>
<string name="streams_notifications_interval_title">কম্পাঙ্ক দেখো</string>
<string name="seekbar_preview_thumbnail_title">পূর্বদর্শন রেখার মাধ্যমে প্রাকদর্শন</string>
<string name="show_image_indicators_title">ছবিরূপ সূচক দেখাও</string>
<string name="dont_show">দেখিও না</string>
<string name="any_network">যেকোনো নেটওয়ার্ক</string>
<string name="enqueued_next">পরেরটা ক্রমে রাখা হয়েছে</string>

View File

@ -625,7 +625,6 @@
<string name="dont_show">No mostris</string>
<string name="low_quality_smaller">Baixa qualitat (més petit)</string>
<string name="high_quality_larger">Alta qualitat (més gran)</string>
<string name="show_image_indicators_title">Mostra indicadors de la imatge</string>
<string name="disable_media_tunneling_summary">Desactiva l\'entunelament del contingut si en els videos hi ha una pantalla negre o tartamudegen</string>
<string name="show_channel_details">Mostra detalls del canal</string>
<string name="no_dir_yet">No s\'ha establert una carpeta de descàrregues, selecciona la carpeta per defecte ara</string>
@ -669,7 +668,6 @@
<string name="detail_pinned_comment_view_description">Comentari fixat</string>
<string name="show_crash_the_player_title">Mostra \"Força el tancament del reproductor\"</string>
<string name="show_crash_the_player_summary">Mostra una opció de fallada quan s\'utilitza el reproductor</string>
<string name="show_image_indicators_summary">Mostra les cintes de color Picasso a la part superior de les imatges que indiquen la seva font: vermell per a la xarxa, blau per al disc i verd per a la memòria</string>
<string name="leak_canary_not_available">El LeakCanary no està disponible</string>
<string name="streams_notifications_interval_title">Comprovant freqüència</string>
<string name="streams_notifications_network_title">Es necesita una conexió a Internet</string>

View File

@ -631,8 +631,6 @@
<string name="dont_show">پیشان نەدرێت</string>
<string name="low_quality_smaller">کواڵێتی نزم (بچووکتر)</string>
<string name="high_quality_larger">کواڵێتی بەرز (گەورەتر)</string>
<string name="show_image_indicators_summary">پیشاندانی شریتە ڕەنگکراوەکانی پیکاسۆ لەسەرووی وێنەکانەوە بۆ بەدیار خستنی سەرچاوەکانیان : سوور بۆ تۆڕ ، شین بۆ دیسک و سەوز بۆ بیرگە</string>
<string name="show_image_indicators_title">پیشاندانی دیارخەرەکانی وێنە</string>
<string name="remote_search_suggestions">پێشنیازکراوەکانی گەڕانی ڕیمۆت</string>
<string name="local_search_suggestions">پێشنیازکراوەکانی گەڕانی نێوخۆیی</string>
<string name="mark_as_watched">دیارکردن وەک بینراو</string>

View File

@ -654,8 +654,6 @@
<item quantity="few">%s stahování dokončena</item>
<item quantity="other">%s stahováních dokončeno</item>
</plurals>
<string name="show_image_indicators_summary">Zobrazit barevné pásky Picasso na obrázcích označujících jejich zdroj: červená pro síť, modrá pro disk a zelená pro paměť</string>
<string name="show_image_indicators_title">Zobrazit indikátory obrázků</string>
<string name="remote_search_suggestions">Vzdálené návrhy vyhledávání</string>
<string name="local_search_suggestions">Lokální návrhy vyhledávání</string>
<string name="start_main_player_fullscreen_summary">Pokud je vypnuté automatické otáčení, nespouštět video v mini přehrávači, ale přepnout se přímo do režimu celé obrazovky. Do mini přehrávače se lze i nadále dostat ukončením režimu celé obrazovky</string>

View File

@ -516,7 +516,6 @@
<string name="processing_may_take_a_moment">Behandler… Det kan tage et øjeblik</string>
<string name="show_memory_leaks">Vis hukommelseslækager</string>
<string name="disable_media_tunneling_title">Deaktivér medietunneling</string>
<string name="show_image_indicators_title">Vis billedindikatorer</string>
<string name="streams_notifications_network_title">Netværkskrav</string>
<string name="any_network">Alle netværk</string>
<string name="streams_notifications_interval_title">Kontrolfrekvens</string>
@ -695,7 +694,6 @@
<string name="faq_title">Ofte stillede spørgsmål</string>
<string name="faq_description">Hvis du har problemer med at bruge appen, bør du tjekke disse svar på almindelige spørgsmål!</string>
<string name="faq">Se på webside</string>
<string name="show_image_indicators_summary">Vis Picasso-farvede bånd oven på billeder, der angiver deres kilde: rød for netværk, blå for disk og grøn for hukommelse</string>
<string name="app_update_unavailable_toast">Du kører den nyeste version af NewPipe</string>
<string name="new_seek_duration_toast">Pga. ExoPlayer-begrænsninger blev søgevarigheden sat til %d sekunder</string>
<string name="feed_group_show_only_ungrouped_subscriptions">Vis kun ikke-grupperede abonnementer</string>

View File

@ -637,8 +637,6 @@
<string name="off">Aus</string>
<string name="mark_as_watched">Als gesehen markieren</string>
<string name="detail_heart_img_view_description">Vom Ersteller mit Herz versehen</string>
<string name="show_image_indicators_summary">Farbige Picasso-Bänder über den Bildern anzeigen, die deren Quelle angeben: rot für Netzwerk, blau für Festplatte und grün für Speicher</string>
<string name="show_image_indicators_title">Bildindikatoren anzeigen</string>
<string name="remote_search_suggestions">Entfernte Suchvorschläge</string>
<string name="local_search_suggestions">Lokale Suchvorschläge</string>
<plurals name="deleted_downloads_toast">

View File

@ -634,8 +634,6 @@
<string name="seekbar_preview_thumbnail_title">Προεπισκόπηση στην μπάρα αναζήτησης</string>
<string name="mark_as_watched">Σήμανση ως αναπαραχθέν</string>
<string name="detail_heart_img_view_description">Επισημάνθηκε από τον δημιουργό</string>
<string name="show_image_indicators_summary">Εμφάνιση χρωματιστής κορδέλας πάνω στις εικόνες, που δείχνει την πηγή τους: κόκκινη για δίκτυο, μπλε για δίσκο και πράσινο για μνήμη</string>
<string name="show_image_indicators_title">Εμφάνιση δεικτών εικόνων</string>
<string name="remote_search_suggestions">Προτάσεις απομακρυσμένης αναζήτησης</string>
<string name="local_search_suggestions">Προτάσεις τοπικής αναζήτησης</string>
<plurals name="deleted_downloads_toast">

View File

@ -647,8 +647,6 @@
<string name="comments_are_disabled">Los comentarios están deshabilitados</string>
<string name="detail_heart_img_view_description">De corazón por el creador</string>
<string name="mark_as_watched">Marcar como visto</string>
<string name="show_image_indicators_summary">Mostrar cintas de colores Picasso encima de las imágenes indicando su origen: rojo para la red, azul para el disco y verde para la memoria</string>
<string name="show_image_indicators_title">Mostrar indicadores de imagen</string>
<string name="remote_search_suggestions">Sugerencias de búsqueda remota</string>
<string name="local_search_suggestions">Sugerencias de búsqueda local</string>
<plurals name="deleted_downloads_toast">

View File

@ -634,8 +634,6 @@
\n
\nNii et valik taandub sellele, mida eelistad: kiirus või täpne teave.</string>
<string name="mark_as_watched">Märgi vaadatuks</string>
<string name="show_image_indicators_summary">Näita piltide kohal Picasso värvides riba, mis märgib pildi allikat: punane tähistab võrku, sinine kohalikku andmekandjat ja roheline kohalikku mälu</string>
<string name="show_image_indicators_title">Näita piltide allikat</string>
<string name="remote_search_suggestions">Kaugotsingu soovitused</string>
<string name="local_search_suggestions">Kohaliku otsingu soovitused</string>
<string name="main_page_content_swipe_remove">Üksuse eemaldamiseks viipa</string>

View File

@ -641,8 +641,6 @@
<item quantity="one">Deskarga amaituta</item>
<item quantity="other">%s Deskarga amaituta</item>
</plurals>
<string name="show_image_indicators_summary">Irudien gainean Picasso koloretako zintak erakutsi, jatorria adieraziz: gorria sarerako, urdina diskorako eta berdea memoriarako</string>
<string name="show_image_indicators_title">Erakutsi irudi-adierazleak</string>
<string name="remote_search_suggestions">Urruneko bilaketaren iradokizunak</string>
<string name="local_search_suggestions">Tokiko bilaketa-iradokizunak</string>
<string name="mark_as_watched">Ikusi gisa markatu</string>

View File

@ -597,7 +597,6 @@
<string name="notification_colorize_title">رنگی کردن آگاهی</string>
<string name="open_with">گشودن با</string>
<string name="mark_as_watched">نشانه به عنوان دیده شده</string>
<string name="show_image_indicators_summary">نمایش روبان‌های رنگی پیکاسو در بالای تصویرها کهنشانگر منبعشان است: قرمز برای شبکه ، آبی برای دیسک و سبز برای حافظه</string>
<string name="notification_colorize_summary">درخواست از اندروید برای سفارشی‌سازی رنگ آگاهی براساس رنگ اصلی در بندانگشتی (توجّه داشته باشید که روی همهٔ افزاره‌ها در دسترس نیست)</string>
<string name="restricted_video">این ویدیو محدود به سن است.
\n
@ -635,7 +634,6 @@
<string name="detail_heart_img_view_description">قلب‌شده به دست ایجادگر</string>
<string name="local_search_suggestions">پیشنهادهای جست‌وجوی محلّی</string>
<string name="remote_search_suggestions">پیشنهادهای جست‌وجوی دوردست</string>
<string name="show_image_indicators_title">نمایش نشانگرهای تصویر</string>
<plurals name="download_finished_notification">
<item quantity="one">بارگیری پایان یافت</item>
<item quantity="other">%s بارگیری پایان یافتند</item>

View File

@ -634,8 +634,6 @@
<string name="no_dir_yet">Latauskansiota ei vielä asetettu, valitse ensin oletuslatauskansio</string>
<string name="comments_are_disabled">Kommentit poistettu käytöstä</string>
<string name="mark_as_watched">Merkitse katsotuksi</string>
<string name="show_image_indicators_summary">Näytä Picasso-värjätyt nauhat kuvien päällä osoittaakseen lähteen: punainen tarkoittaa verkkoa, sininen tarkoittaa levytilaa ja vihreä tarkoittaa muistia</string>
<string name="show_image_indicators_title">Näytä kuvailmaisimet</string>
<string name="remote_search_suggestions">Etähakuehdotukset</string>
<string name="local_search_suggestions">Paikalliset hakuehdotukset</string>
<string name="enqueue_next_stream">Lisää seuraavaksi</string>

View File

@ -646,10 +646,8 @@
<string name="seekbar_preview_thumbnail_title">Prévisualisation de la barre de progression sur la miniature</string>
<string name="mark_as_watched">Marquer comme visionné</string>
<string name="detail_heart_img_view_description">Apprécié par le créateur</string>
<string name="show_image_indicators_title">Afficher les indicateurs dimage</string>
<string name="remote_search_suggestions">Suggestions de recherche distante</string>
<string name="local_search_suggestions">Suggestions de recherche locale</string>
<string name="show_image_indicators_summary">Affiche les rubans colorés de Picasso au-dessus des images indiquant leur source : rouge pour le réseau, bleu pour le disque et vert pour la mémoire</string>
<plurals name="deleted_downloads_toast">
<item quantity="one">%1$s téléchargement supprimé</item>
<item quantity="many">%1$s téléchargements supprimés</item>

View File

@ -627,7 +627,6 @@
<item quantity="other">%s descargas finalizadas</item>
</plurals>
<string name="seekbar_preview_thumbnail_title">Miniatura na barra de busca</string>
<string name="show_image_indicators_title">Mostrar indicadores de imaxe</string>
<string name="disable_media_tunneling_summary">Desactive o túnel multimedia se experimentar unha pantalla en negro ou interrupcións na reprodución.</string>
<string name="disable_media_tunneling_title">Desactivar túnel multimedia</string>
<string name="enqueued">Engadido á cola</string>
@ -664,7 +663,6 @@
<string name="downloads_storage_use_saf_summary_api_29">A partir do Android 10, só o \'Sistema de Acceso ao Almacenamento\' está soportado</string>
<string name="processing_may_take_a_moment">Procesando... Pode devagar un momento</string>
<string name="create_error_notification">Crear unha notificación de erro</string>
<string name="show_image_indicators_summary">Amosar fitas coloridas de Picasso na cima das imaxes que indican a súa fonte: vermello para a rede, azul para o disco e verde para a memoria</string>
<string name="feed_new_items">Novos elementos</string>
<string name="progressive_load_interval_exoplayer_default">Predefinido do ExoPlayer</string>
<string name="show_crash_the_player_title">Amosar \"Travar o reprodutor\"</string>

View File

@ -654,8 +654,6 @@
<string name="seekbar_preview_thumbnail_title">תמונה מוקטנת בסרגל הנגינה</string>
<string name="detail_heart_img_view_description">סומן בלב על ידי היוצר</string>
<string name="mark_as_watched">סימון כנצפה</string>
<string name="show_image_indicators_summary">הצגת סרטים בסגנון פיקאסו בראש התמונות לציון המקור שלהם: אדום זה מהרשת, כחול מהכונן וירוק מהזיכרון</string>
<string name="show_image_indicators_title">הצגת מחווני תמונות</string>
<string name="remote_search_suggestions">הצעות חיפוש מרוחקות</string>
<string name="local_search_suggestions">הצעות חיפוש מקומיות</string>
<plurals name="deleted_downloads_toast">

View File

@ -570,7 +570,6 @@
<string name="enqueued">कतारबद्ध हुआ</string>
<string name="loading_stream_details">स्ट्रीम विवरण लोड हो रहे हैं…</string>
<string name="processing_may_take_a_moment">प्रोसेस हो रहा है… कुछ समय लग सकता है</string>
<string name="show_image_indicators_title">छवि संकेतक दिखाएं</string>
<string name="show_crash_the_player_summary">प्लेयर का उपयोग करते समय क्रैश विकल्प दिखाता है</string>
<string name="check_new_streams">नई स्ट्रीमों के लिए जांच चलाएं</string>
<string name="show_error_snackbar">एक त्रुटि स्नैकबार दिखाएं</string>
@ -668,7 +667,6 @@
<string name="leak_canary_not_available">लीक-कैनरी उपलब्ध नहीं है</string>
<string name="error_report_notification_toast">एक त्रुटी हुई है, नोटीफिकेशन देखें</string>
<string name="disable_media_tunneling_summary">यदि वीडियो प्लेबैक पर आप काली स्क्रीन या रुक-रुक कर वीडियो चलने का अनुभव करते हैं तो मीडिया टनलिंग को अक्षम करें।</string>
<string name="show_image_indicators_summary">छवियों के शीर्ष पर पिकासो रंगीन रिबन दिखाएँ जो उनके स्रोत को दर्शाता है: नेटवर्क के लिए लाल, डिस्क के लिए नीला और मेमोरी के लिए हरा</string>
<string name="create_error_notification">त्रुटी की नोटीफिकेशन बनाएं</string>
<string name="error_download_resource_gone">इस डाउनलोड को पुनर्प्राप्त नहीं किया जा सकता</string>
<string name="no_dir_yet">अभी तक कोई डाउनलोड फ़ोल्डर सेट नहीं किया गया है, अब डिफ़ॉल्ट डाउनलोड फ़ोल्डर चुनें</string>

View File

@ -646,7 +646,6 @@
<string name="service_provides_reason">%s pruža ovaj razlog:</string>
<string name="processing_may_take_a_moment">Obrada u tijeku … Može malo potrajati</string>
<string name="main_page_content_swipe_remove">Za ukljanjanje stavki povuci ih</string>
<string name="show_image_indicators_title">Prikaži indikatore slike</string>
<plurals name="download_finished_notification">
<item quantity="one">Preuzimanje je gotovo</item>
<item quantity="few">%s preuzimanja su gotova</item>
@ -655,7 +654,6 @@
<string name="start_main_player_fullscreen_title">Pokreni glavni player u cjeloekranskom prikazu</string>
<string name="enqueue_next_stream">Dodaj u popis kao sljedeći</string>
<string name="enqueued_next">Dodano u popis kao sljedeći</string>
<string name="show_image_indicators_summary">Prikaži Picassove vrpce u boji na slikama koje označavaju njihov izvor: crvena za mrežu, plava za disk i zelena za memoriju</string>
<plurals name="deleted_downloads_toast">
<item quantity="one">Izbrisano %1$s preuzimanje</item>
<item quantity="few">Izbrisana %1$s preuzimanja</item>

View File

@ -570,7 +570,6 @@
<string name="enable_disposed_exceptions_summary">Az eltávolítás utáni, fragment vagy activity életcikluson kívüli, nem kézbesíthető Rx kivételek jelentésének kényszerítése</string>
<string name="show_original_time_ago_title">Eredeti „ennyi ideje” megjelenítése az elemeken</string>
<string name="disable_media_tunneling_summary">Tiltsa le a médiacsatornázást, ha fekete képernyőt vagy akadozást tapasztal videólejátszáskor.</string>
<string name="show_image_indicators_summary">Picasso színes szalagok megjelenítése a képek fölött, megjelölve a forrásukat: piros a hálózathoz, kék a lemezhez, zöld a memóriához</string>
<string name="downloads_storage_ask_summary_no_saf_notice">Minden letöltésnél meg fogja kérdezni, hogy hova mentse el</string>
<string name="choose_instance_prompt">Válasszon egy példányt</string>
<string name="feed_oldest_subscription_update">Lista legutóbbi frissítése: %s</string>
@ -657,7 +656,6 @@
\nBiztos benne\? Ez nem vonható vissza!</string>
<string name="show_original_time_ago_summary">A szolgáltatásokból származó eredeti szövegek láthatók lesznek a közvetítési elemeken</string>
<string name="crash_the_player">Lejátszó összeomlasztása</string>
<string name="show_image_indicators_title">Képjelölők megjelenítése</string>
<string name="show_crash_the_player_title">A „lejátszó összeomlasztása” lehetőség megjelenítése</string>
<string name="show_crash_the_player_summary">Megjeleníti az összeomlasztási lehetőséget a lejátszó használatakor</string>
<string name="unhook_checkbox">Hangmagasság megtartása (torzítást okozhat)</string>

View File

@ -631,13 +631,11 @@
<string name="detail_heart_img_view_description">Disukai oleh kreator</string>
<string name="local_search_suggestions">Saran pencarian lokal</string>
<string name="remote_search_suggestions">Saran pencarian remote</string>
<string name="show_image_indicators_title">Tampilkan indikator gambar</string>
<plurals name="deleted_downloads_toast">
<item quantity="other">Menghapus %1$s unduhan</item>
</plurals>
<string name="enqueue_next_stream">tambahkan ke selanjutnya</string>
<string name="enqueued_next">telah ditambahkan ke selanjutnya</string>
<string name="show_image_indicators_summary">Tampilkan Ribon bewarna Picasso di atas gambar yang mengindikasikan asalnya: merah untuk jaringan, biru untuk disk dan hijau untuk memori</string>
<string name="start_main_player_fullscreen_summary">Jangan memulai memutar video di mini player, tapi nyalakan langsung di mode layar penuh, jika rotasi otomatis terkunci. Anda tetap dapat mengakses mini player dengan keluar dari layar penuh</string>
<string name="processing_may_take_a_moment">Memproses… Mungkin butuh waktu sebentar</string>
<string name="check_for_updates">Periksa Pembaruan</string>

View File

@ -657,8 +657,6 @@
<string name="show_original_time_ago_summary">Upprunalegir textar frá þjónustu verða sýnilegir í atriðum</string>
<string name="disable_media_tunneling_title">Slökkva á fjölmiðlagöngum</string>
<string name="disable_media_tunneling_summary">Slökktu á fjölmiðlagöngum ef þú finnur fyrir svörtum skjá eða stami við spilun myndbandar</string>
<string name="show_image_indicators_title">Sýna myndvísa</string>
<string name="show_image_indicators_summary">Sýna Picasso litaða borða ofan á myndum sem gefa til kynna uppruna þeirra: rauðan fyrir netið, bláan fyrir disk og grænan fyrir minni</string>
<string name="show_crash_the_player_title">Sýna „Hrynja spilara“</string>
<string name="show_crash_the_player_summary">Sýna valkost til að hrynja spilara</string>
<string name="crash_the_app">Hrynja forrit</string>

View File

@ -644,8 +644,6 @@
<string name="comments_are_disabled">Commenti disattivati</string>
<string name="detail_heart_img_view_description">Apprezzato dall\'autore</string>
<string name="mark_as_watched">Segna come visto</string>
<string name="show_image_indicators_summary">Mostra gli indicatori colorati Picasso sopra le immagini, per indicare la loro fonte: rosso per la rete, blu per il disco e verde per la memoria</string>
<string name="show_image_indicators_title">Mostra indicatori immagine</string>
<string name="remote_search_suggestions">Suggerimenti di ricerca remoti</string>
<string name="local_search_suggestions">Suggerimenti di ricerca locali</string>
<plurals name="deleted_downloads_toast">

View File

@ -634,8 +634,6 @@
<plurals name="download_finished_notification">
<item quantity="other">%s つのダウンロードが完了しました</item>
</plurals>
<string name="show_image_indicators_summary">ピカソは、画像の上に、画像の出所を識別する色彩記章を表示します: 赤はネットワーク、青はディスク、緑はメモリ</string>
<string name="show_image_indicators_title">画像に標識を表示</string>
<string name="processing_may_take_a_moment">処理中… 少し時間がかかるかもしれません</string>
<string name="manual_update_description">新しいバージョンを手動で確認します</string>
<string name="checking_updates_toast">アップデートを確認中…</string>

View File

@ -384,7 +384,6 @@
<string name="show_original_time_ago_summary">ორიგინალური ტექსტები სერვისებიდან ხილული იქნება ნაკადის ერთეულებში</string>
<string name="disable_media_tunneling_title">მედია გვირაბის გათიშვა</string>
<string name="disable_media_tunneling_summary">გამორთეთ მედია გვირაბი, თუ ვიდეოს დაკვრისას შავი ეკრანი ან ჭუჭყი გაქვთ</string>
<string name="show_image_indicators_title">გამოსახულების ინდიკატორების ჩვენება</string>
<string name="show_crash_the_player_title">აჩვენე \"დამკვრელის დამსხვრევა\"</string>
<string name="show_crash_the_player_summary">აჩვენებს ავარიის ვარიანტს დამკვრელის გამოყენებისას</string>
<string name="check_new_streams">გაუშვით შემოწმება ახალი ნაკადებისთვის</string>
@ -683,7 +682,6 @@
<string name="subscriptions_import_unsuccessful">გამოწერების იმპორტი ვერ მოხერხდა</string>
<string name="enable_disposed_exceptions_title">შეატყობინეთ სასიცოცხლო ციკლის შეცდომებს</string>
<string name="enable_disposed_exceptions_summary">იძულებითი მოხსენება შეუსაბამო Rx გამონაკლისების შესახებ ფრაგმენტის ან აქტივობის სასიცოცხლო ციკლის გარეთ განკარგვის შემდეგ</string>
<string name="show_image_indicators_summary">აჩვენეთ პიკასოს ფერადი ლენტები სურათების თავზე, სადაც მითითებულია მათი წყარო: წითელი ქსელისთვის, ლურჯი დისკისთვის და მწვანე მეხსიერებისთვის</string>
<string name="feed_load_error_fast_unknown">სწრაფი კვების რეჟიმი ამაზე მეტ ინფორმაციას არ იძლევა.</string>
<string name="feed_load_error_account_info">„%s“-ის არხის ჩატვირთვა ვერ მოხერხდა.</string>
<string name="feed_use_dedicated_fetch_method_summary">ხელმისაწვდომია ზოგიერთ სერვისში, როგორც წესი, ბევრად უფრო სწრაფია, მაგრამ შეიძლება დააბრუნოს შეზღუდული რაოდენობის ელემენტი და ხშირად არასრული ინფორმაცია (მაგ. ხანგრძლივობის გარეშე, ელემენტის ტიპი, არ არის ლაივის სტატუსი)</string>

View File

@ -545,8 +545,6 @@
<string name="disable_media_tunneling_title">미디어 터널링 비활성화</string>
<string name="show_original_time_ago_summary">서비스의 원본 텍스트가 스트림 항목에 표시됩니다</string>
<string name="disable_media_tunneling_summary">동영상 재생 시 검은 화면이 나타나거나 끊김 현상이 발생하면 미디어 터널링을 비활성화하세요.</string>
<string name="show_image_indicators_title">이미지 표시기 표시</string>
<string name="show_image_indicators_summary">원본을 나타내는 이미지 위에 피카소 컬러 리본 표시: 네트워크는 빨간색, 디스크는 파란색, 메모리는 녹색</string>
<string name="show_crash_the_player_title">\"플레이어 충돌\" 표시</string>
<string name="show_crash_the_player_summary">플레이어를 사용할 때 충돌 옵션을 표시합니다</string>
<string name="check_new_streams">새로운 스트림 확인 실행</string>

View File

@ -644,8 +644,6 @@
<string name="dont_show">Nerodyti</string>
<string name="detail_heart_img_view_description">Širdelė nuo kurėjo</string>
<string name="mark_as_watched">Pažymėti kaip peržiūrėtą</string>
<string name="show_image_indicators_summary">Rodyti „Picasso“ spalvotas juosteles ant vaizdų, nurodančių jų šaltinį: raudona tinklui, mėlyna diskui ir žalia atmintis</string>
<string name="show_image_indicators_title">Rodyti vaizdo indikatorius</string>
<string name="remote_search_suggestions">Nuotolinės paieškos pasiūlymai</string>
<string name="local_search_suggestions">Vietinės paieškos pasiūlymai</string>
<plurals name="deleted_downloads_toast">

View File

@ -625,12 +625,10 @@
<string name="start_main_player_fullscreen_summary">Nesākt video atskaņošanu samazinātā režīmā, bet pilnekrāna režīmā, ja automātiskā rotācija ir izslēgta</string>
<string name="disable_media_tunneling_title">Izslēgt multivides tuneļošanu</string>
<string name="disable_media_tunneling_summary">Izslēdziet multivides tuneļošanu, ja jums video atskaņošanas laikā parādās melns ekrāns vai aizķeršanās</string>
<string name="show_image_indicators_summary">Rādīt krāsainas lentes virs attēliem, norādot to avotu: sarkana - tīkls, zila - disks, zaļa - atmiņa</string>
<string name="description_select_enable">Ieslēgt teksta atlasīšanu video aprakstā</string>
<string name="no_dir_yet">Lejupielādes mape vēl nav iestatīta, izvēlieties noklusējuma lejupielādes mapi</string>
<string name="main_page_content_swipe_remove">Pārvelciet objektus, lai tos noņemtu</string>
<string name="local_search_suggestions">Lokālie meklēšanas ieteikumi</string>
<string name="show_image_indicators_title">Rādīt attēlu indikatorus</string>
<string name="high_quality_larger">Augstas kvalitātes (lielāks)</string>
<string name="check_for_updates">Pārbaudīt atjauninājumus</string>
<string name="manual_update_description">Manuāli pārbaudīt, vai ir atjauninājumi</string>

View File

@ -595,7 +595,6 @@
<string name="notification_action_1_title">രണ്ടാം പ്രവർത്തന ബട്ടൺ</string>
<string name="notification_action_0_title">ആദ്യ പ്രവർത്തന ബട്ടൺ</string>
<string name="disable_media_tunneling_summary">വീഡിയോ കാണുമ്പോൾ കറുത്ത സ്ക്രീൻ, അവ്യക്തത അനുഭവിക്കുന്നു എങ്കിൽ മീഡിയ ട്യൂൺലിങ് പ്രവർത്തനരഹിതമാക്കുക</string>
<string name="show_image_indicators_summary">ഉറവിടം തിരിച്ചറിയാൻ പിക്കാസോ കളർഡ് റിബൺ ചിത്രങ്ങളുടെ മുകളിൽ കാണിക്കുക: നെറ്റ്‌വർക്കിന് ചുവപ്പ്, ഡിസ്കിനു നീല, മെമ്മറിയിക്ക് പച്ച</string>
<string name="seekbar_preview_thumbnail_title">സീക്ബാർ ചെറുചിത്രം പ്രദർശനം</string>
<string name="detail_heart_img_view_description">സ്നേഹത്തോടെ സൃഷ്ടാവ്</string>
<string name="description_select_disable">ഡിസ്ക്രിപ്ഷനിലെ ടെക്സ്റ്റ്‌ സെലക്ട്‌ ചെയ്യുവാൻ അനുവദിക്കാതെ ഇരിക്കുക</string>
@ -630,7 +629,6 @@
<string name="dont_show">കാണിക്കരുത്</string>
<string name="low_quality_smaller">കുറഞ്ഞ നിലവാരം (ചെറുത് )</string>
<string name="high_quality_larger">ഉയർന്ന നിലവാരം (വലിയത് )</string>
<string name="show_image_indicators_title">ഇമേജ് ഇൻഡിക്കേറ്ററുകൾ കാണിക്കുക</string>
<string name="disable_media_tunneling_title">മീഡിയ ട്യൂൺലിങ് പ്രവർത്തനരഹിതമാക്കുക</string>
<string name="no_dir_yet">ഡൌൺലോഡ് ഫോൾഡർ ഇത് വരെയും സെറ്റ് ചെയ്തിട്ടില്ല, സ്ഥിര ഡൌൺലോഡ് ഫോൾഡർ ഇപ്പോൾ തിരഞ്ഞെക്കുക</string>
<string name="comments_are_disabled">അഭിപ്രായങ്ങൾ പ്രവർത്തനരഹിതമായിരിക്കുന്നു</string>

View File

@ -644,9 +644,7 @@
<string name="local_search_suggestions">Lokale søkeforslag</string>
<string name="mark_as_watched">Marker som sett</string>
<string name="start_main_player_fullscreen_summary">Ikke start videoer i minispilleren, men bytt til fullskjermsmodus direkte dersom auto-rotering er låst. Du har fremdeles tilgang til minispilleren ved å avslutte fullskjermsvisningen</string>
<string name="show_image_indicators_summary">Vis Picasso-fargede bånd på toppen av bilder for å indikere kilde: Rød for nettverk, blå for disk, og grønn for minne</string>
<string name="detail_heart_img_view_description">Hjertemerket av skaperen</string>
<string name="show_image_indicators_title">Vis bildeindikatorer</string>
<string name="main_page_content_swipe_remove">Dra elementer for å fjerne dem</string>
<string name="start_main_player_fullscreen_title">Start hovedspiller i fullskjerm</string>
<string name="enqueue_next_stream">Still i kø neste</string>

View File

@ -611,7 +611,6 @@
<string name="no_dir_yet">Geen download map ingesteld, kies nu de standaard download map</string>
<string name="dont_show">Niet tonen</string>
<string name="comments_are_disabled">Reacties zijn uitgeschakeld</string>
<string name="show_image_indicators_title">Toon afbeeldingsindicatoren</string>
<string name="settings_category_player_notification_title">Speler melding</string>
<string name="settings_category_player_notification_summary">Configureer actieve stream melding</string>
<string name="notifications">Meldingen</string>

View File

@ -632,8 +632,6 @@
<string name="low_quality_smaller">Lage kwaliteit (kleiner)</string>
<string name="high_quality_larger">Hoge kwaliteit (groter)</string>
<string name="seekbar_preview_thumbnail_title">Zoekbalk miniatuurafbeelding voorbeeld</string>
<string name="show_image_indicators_summary">Toon Picasso-gekleurde linten bovenop afbeeldingen die hun bron aangeven: rood voor netwerk, blauw voor schijf en groen voor geheugen</string>
<string name="show_image_indicators_title">Afbeeldings­indicatoren tonen</string>
<string name="comments_are_disabled">Reacties zijn uitgeschakeld</string>
<string name="remote_search_suggestions">Zoeksuggesties op afstand</string>
<string name="local_search_suggestions">Lokale zoeksuggesties</string>

View File

@ -440,8 +440,6 @@
<string name="show_original_time_ago_summary">ߗߋߢߊߟߌ ߟߎ߬ ߞߟߏߜߍ߫ ߓߐߛߎ߲ߡߊ ߟߎ߬ ߦߌ߬ߘߊ߬ߕߐ߫ ߟߋ߬ ߟߊ߬ߖߍ߲߬ߛߍ߲߬ߠߌ߲ ߘߐ߫</string>
<string name="disable_media_tunneling_title">ߞߋߟߋߞߋߟߋ ߟߊ߫ ߝߊߟߊ߲ߓߍߦߊ ߟߊߛߊ߬</string>
<string name="disable_media_tunneling_summary">ߞߋߟߋߞߋߟߋ ߟߊ߫ ߝߊߟߊ߲ߓߍߦߊ ߟߊߛߊ߬ ߣߴߌ ߞߊ߬ ߥߊ߲߬ߊߥߊ߲߬ ߝߌ߲ ߦߋ߫ ߥߟߊ߫ ߜߊߘߊ߲ߜߊߘߊ߲ߠߌ߲ ߦߋߡߍ߲ߕߊ ߘߏ߫ ߘߐߛߊߙߌ߫ ߕߎߡߊ</string>
<string name="show_image_indicators_title">ߞߊ߬ ߖߌ߬ߦߊ߬ߓߍ߫ ߦߌ߬ߘߊ߬ߟߊ߲ ߠߎ߫ ߝߍ߲߬ߛߍ߲߫</string>
<string name="show_image_indicators_summary">ߏ߬ ߦߋ߫ ߔߌߛߊߞߏ߫ ߟߊ߫ ߡߙߎߝߋ߫ ߞߟߐ߬ߡߊ ߟߎ߫ ߟߋ߬ ߝߍ߲߬ߛߍ߲߬ ߠߊ߫ ߖߌ߬ߦߊ߬ߓߍ ߟߎ߫ ߞߎ߲߬ߘߐ߫ ߞߵߊ߬ߟߎ߬ ߓߐߛߎ߲ ߦߌ߬ߘߊ߬: ߥߎߟߋ߲߬ߡߊ߲ ߦߋ߫ ߞߙߏ߬ߝߏ ߕߊ ߘߌ߫߸ ߓߊ߯ߡߊ ߦߋ߫ ߝߘߍ߬ ߜߍߟߍ߲ ߕߊ ߘߌ߫ ߊ߬ߣߌ߫ ߝߙߌߛߌߡߊ ߦߋ߫ ߦߟߌߕߏߟߊ߲ ߕߊ ߘߌ߫</string>
<string name="export_ongoing">ߟߊ߬ߓߐ߬ߟߌ ߦߴߌߘߐ߫…</string>
<string name="start">ߞߵߊ߬ ߘߊߡߌ߬ߣߊ߬</string>
<string name="comments_are_disabled">ߞߊ߲߬ߞߎߡߊ ߟߎ߬ ߟߊߛߊ߬ߣߍ߲ ߠߋ߬</string>

View File

@ -342,7 +342,6 @@
<string name="show_original_time_ago_summary">ସେବାଗୁଡିକରୁ ମୂଳ ଲେଖା ଷ୍ଟ୍ରିମ୍ ଆଇଟମ୍ ଗୁଡିକରେ ଦୃଶ୍ୟମାନ ହେବ</string>
<string name="disable_media_tunneling_title">ମିଡିଆ ଟନେଲିଂକୁ ଅକ୍ଷମ କରନ୍ତୁ</string>
<string name="disable_media_tunneling_summary">ଯଦି ଆପଣ ଏକ କଳା ପରଦା ଅନୁଭବ କରନ୍ତି କିମ୍ବା ଭିଡିଓ ପ୍ଲେବେକ୍ ଉପରେ ଝୁଣ୍ଟି ପଡ଼ନ୍ତି ତେବେ ମିଡିଆ ଟନେଲିଂକୁ ଅକ୍ଷମ କରନ୍ତୁ ।</string>
<string name="show_image_indicators_summary">ଚିତ୍ରଗୁଡ଼ିକର ଉପରେ ପିକାସୋ ରଙ୍ଗୀନ ଫିତା ଦେଖାନ୍ତୁ: ସେମାନଙ୍କର ଉତ୍ସକୁ ସୂଚାଇଥାଏ: ନେଟୱାର୍କ ପାଇଁ ନାଲି, ଡିସ୍କ ପାଇଁ ନୀଳ ଏବଂ ସ୍ମୃତି ପାଇଁ ସବୁଜ</string>
<string name="import_title">ଆମଦାନି କରନ୍ତୁ</string>
<string name="import_from">ଠାରୁ ଆମଦାନୀ କରନ୍ତୁ</string>
<string name="import_ongoing">ଆମଦାନି…</string>
@ -657,7 +656,6 @@
<string name="playlist_no_uploader">ଅଟୋ-ଜେନେରେଟ୍ (କୌଣସି ଅପଲୋଡର୍ ମିଳିଲା ନାହିଁ)</string>
<string name="resize_fill">ପୁରଣ କରନ୍ତୁ</string>
<string name="caption_setting_title">କ୍ୟାପସନ୍</string>
<string name="show_image_indicators_title">ପ୍ରତିଛବି ସୂଚକ ଦେଖାନ୍ତୁ</string>
<string name="show_crash_the_player_summary">ପ୍ଲେୟାର ବ୍ୟବହାର କରିବା ସମୟରେ ଏକ କ୍ରାସ୍ ବିକଳ୍ପ ଦେଖାଏ</string>
<string name="check_new_streams">ନୂତନ ଷ୍ଟ୍ରିମ୍ ପାଇଁ ଯାଞ୍ଚ ଚଲାନ୍ତୁ</string>
<string name="crash_the_app">ଆପ୍ କ୍ରାସ୍ କରନ୍ତୁ</string>

View File

@ -626,8 +626,6 @@
<string name="error_report_notification_title">ਨਿਊਪਾਈਪ ਖਾਮੀ ਤੋਂ ਪ੍ਰਭਾਵਤ ਹੋਈ ਹੈ, ਇੱਥੇ ਨੱਪ ਕੇ ਰਿਪੋਰਟ ਕਰੋ</string>
<string name="error_report_notification_toast">ਇੱਕ ਖਾਮੀ ਪ੍ਰਭਾਵੀ ਹੋਈ ਹੈ, ਨੋਟੀਫੀਕੇਸ਼ਨ ਵੇਖੋ</string>
<string name="main_page_content_swipe_remove">ਆਈਟਮਾਂ ਨੂੰ ਇੱਕ ਪਾਸੇ ਖਿੱਚ ਕੇ ਹਟਾਓ</string>
<string name="show_image_indicators_title">ਦ੍ਰਿਸ਼ ਸੂਚਕ ਵਿਖਾਓ</string>
<string name="show_image_indicators_summary">ਦ੍ਰਿਸ਼ਾਂ ਦੇ ਉੱਪਰ ਉਹਨਾਂ ਦੀ ਸਰੋਤ-ਪਛਾਣ ਲਈ ਪਿਕਾਸੋ ਦੇ ਰੰਗਦਾਰ ਰਿਬਨ ਵਿਖਾਓ : ਨੈੱਟਵਰਕ ਲਈ ਲਾਲ, ਡਿਸਕ ਲਈ ਨੀਲੇ ਤੇ ਮੈਮਰੀ ਲਈ ਹਰੇ</string>
<string name="enable_streams_notifications_title">ਨਵੀਂ ਸਟ੍ਰੀਮ ਦੇ ਨੋਟੀਫਿਕੇਸ਼ਨ</string>
<string name="detail_pinned_comment_view_description">ਪਿੰਨ ਕੀਤੀ ਟਿੱਪਣੀ</string>
<string name="checking_updates_toast">ਅੱਪਡੇਟ ਦੀ ਉਪਲੱਬਧਤਾ ਪਰਖੀ ਜਾ ਰਹੀ…</string>

View File

@ -649,8 +649,6 @@
<string name="dont_show">Nie pokazuj</string>
<string name="detail_heart_img_view_description">Serduszko od twórcy</string>
<string name="mark_as_watched">Oznacz jako obejrzane</string>
<string name="show_image_indicators_summary">Pokazuj kolorowe wstążki Picasso nad obrazami wskazujące ich źródło: czerwone dla sieci, niebieskie dla dysku i zielone dla pamięci</string>
<string name="show_image_indicators_title">Pokazuj wskaźniki obrazu</string>
<string name="remote_search_suggestions">Zdalne podpowiedzi wyszukiwania</string>
<string name="local_search_suggestions">Lokalne podpowiedzi wyszukiwania</string>
<plurals name="deleted_downloads_toast">

View File

@ -644,7 +644,6 @@
<string name="comments_are_disabled">Os comentários estão desabilitados</string>
<string name="mark_as_watched">Marcar como assistido</string>
<string name="detail_heart_img_view_description">Curtido pelo criador</string>
<string name="show_image_indicators_summary">Exibir fitas coloridas no topo das imagens indicando sua fonte: vermelho para rede, azul para disco e verde para memória</string>
<plurals name="deleted_downloads_toast">
<item quantity="one">%1$s download excluído</item>
<item quantity="many">%1$s downloads excluídos</item>
@ -655,7 +654,6 @@
<item quantity="many">%s downloads concluídos</item>
<item quantity="other">%s downloads concluídos</item>
</plurals>
<string name="show_image_indicators_title">Mostrar indicadores de imagem</string>
<string name="enqueued_next">Adicionado na próxima posição da fila</string>
<string name="enqueue_next_stream">Enfileira a próxima</string>
<string name="main_page_content_swipe_remove">Deslize os itens para remove-los</string>

View File

@ -644,8 +644,6 @@
<string name="no_dir_yet">Ainda não foi definida uma pasta de descarregamento, escolha agora a pasta de descarregamento padrão</string>
<string name="comments_are_disabled">Comentários estão desativados</string>
<string name="mark_as_watched">Marcar como visto</string>
<string name="show_image_indicators_summary">Mostrar fitas coloridas de Picasso em cima das imagens que indicam a sua fonte: vermelho para rede, azul para disco e verde para memória</string>
<string name="show_image_indicators_title">Mostrar indicadores de imagem</string>
<string name="remote_search_suggestions">Sugestões de pesquisa remotas</string>
<string name="local_search_suggestions">Sugestões de pesquisa locais</string>
<plurals name="deleted_downloads_toast">

View File

@ -644,8 +644,6 @@
<string name="low_quality_smaller">Baixa qualidade (menor)</string>
<string name="high_quality_larger">Alta qualidade (maior)</string>
<string name="comments_are_disabled">Os comentários estão desativados</string>
<string name="show_image_indicators_summary">Mostrar fitas coloridas de Picasso em cima das imagens que indicam a sua fonte: vermelho para rede, azul para disco e verde para memória</string>
<string name="show_image_indicators_title">Mostrar indicadores de imagem</string>
<string name="remote_search_suggestions">Sugestões de pesquisa remotas</string>
<string name="local_search_suggestions">Sugestões de pesquisa locais</string>
<plurals name="deleted_downloads_toast">

View File

@ -660,8 +660,6 @@
<string name="low_quality_smaller">Calitate scăzută (mai mică)</string>
<string name="high_quality_larger">Calitate înaltă (mai mare)</string>
<string name="seekbar_preview_thumbnail_title">Miniatură de previzualizare în bara de derulare</string>
<string name="show_image_indicators_summary">Afișați panglici colorate de Picasso deasupra imaginilor, indicând sursa acestora: roșu pentru rețea, albastru pentru disc și verde pentru memorie</string>
<string name="show_image_indicators_title">Afișați indicatorii de imagine</string>
<string name="disable_media_tunneling_summary">Dezactivați tunelarea media dacă întâmpinați un ecran negru sau blocaje la redarea video.</string>
<string name="processing_may_take_a_moment">Procesarea.. Poate dura un moment</string>
<string name="check_for_updates">Verifică dacă există actualizări</string>

View File

@ -652,8 +652,6 @@
<string name="seekbar_preview_thumbnail_title">Миниатюра над полосой прокрутки</string>
<string name="detail_heart_img_view_description">Автору видео понравилось это</string>
<string name="mark_as_watched">Пометить проигранным</string>
<string name="show_image_indicators_summary">Picasso: указать цветом источник изображений (красный — сеть, синий — диск, зелёный — память)</string>
<string name="show_image_indicators_title">Цветные метки на изображениях</string>
<string name="remote_search_suggestions">Серверные предложения поиска</string>
<string name="local_search_suggestions">Локальные предложения поиска</string>
<plurals name="deleted_downloads_toast">

View File

@ -646,8 +646,6 @@
<item quantity="one">%sちぬダウンロードぬかんりょうさびたん</item>
<item quantity="other">%sちぬダウンロードぬかんりょうさびたん</item>
</plurals>
<string name="show_image_indicators_summary">ピカソー、がぞうぬういに、がぞうくとぅどぅくるしーきびちするしきさいきしーょうひょうじさびーん: あかーネットワーク、あおーディスク、みどぅれーメモリ</string>
<string name="show_image_indicators_title">やしがぞうんかいふぃいょうしきひょうじ</string>
<string name="processing_may_take_a_moment">しーょりちゅう… くーてーんじがんがかかいんかむしりやびらん</string>
<string name="manual_update_description">みーさるバージョンしーゅどうでぃかくにんさびーん</string>
<string name="checking_updates_toast">アップデートかくにんちゅう…</string>

View File

@ -270,7 +270,6 @@
<string name="leak_canary_not_available">LeakCanary ᱵᱟᱭ ᱧᱟᱢᱚᱜ ᱠᱟᱱᱟ</string>
<string name="enable_leak_canary_summary">ᱢᱮᱢᱚᱨᱤ ᱞᱤᱠᱟᱞ ᱢᱚᱱᱤᱴᱚᱨᱤᱝ ᱦᱤᱯ ᱰᱟᱢᱯᱤᱝ ᱚᱠᱛᱚ ᱨᱮ ᱮᱯᱞᱤᱠᱮᱥᱚᱱ ᱨᱟᱥᱴᱨᱤᱭ ᱦᱩᱭ ᱫᱟᱲᱮᱭᱟᱜ-ᱟ</string>
<string name="enable_disposed_exceptions_title">ᱡᱤᱭᱚᱱ ᱪᱤᱠᱤ ᱠᱷᱚᱱ ᱵᱟᱦᱨᱮ ᱨᱮ ᱵᱷᱮᱜᱟᱨ ᱠᱚ ᱚᱱᱚᱞ ᱢᱮ</string>
<string name="show_image_indicators_summary">ᱱᱮᱴᱣᱟᱨᱠ ᱞᱟᱹᱜᱤᱫ red, ᱰᱤᱥᱠ ᱞᱟᱹᱜᱤᱫ blue ᱟᱨ ᱢᱮᱢᱚᱨᱤ ᱞᱟᱹᱜᱤᱫ green</string>
<string name="show_crash_the_player_summary">ᱯᱞᱮᱭᱟᱨ ᱵᱮᱵᱷᱟᱨ ᱚᱠᱛᱮ ᱨᱮ ᱠᱨᱟᱥ ᱚᱯᱥᱚᱱ ᱧᱮᱞᱚᱜ ᱠᱟᱱᱟ</string>
<string name="import_title">ᱤᱢᱯᱳᱨᱴ</string>
<string name="import_from">ᱤᱢᱯᱚᱨᱴ</string>
@ -540,7 +539,6 @@
<string name="show_original_time_ago_title">ᱡᱤᱱᱤᱥ ᱠᱚᱨᱮᱱᱟᱜ ᱢᱩᱞ ᱚᱠᱛᱚ ᱧᱮᱞ ᱢᱮ</string>
<string name="show_original_time_ago_summary">ᱥᱮᱵᱟ ᱠᱷᱚᱱ ᱚᱨᱡᱤᱱᱤᱭᱟᱞ ᱴᱮᱠᱥᱴ ᱠᱚ ᱥᱴᱨᱤᱢ ᱤᱴᱮᱢ ᱨᱮ ᱧᱮᱞᱚᱜᱼᱟ</string>
<string name="disable_media_tunneling_summary">ᱡᱩᱫᱤ ᱟᱢ ᱵᱷᱤᱰᱤᱭᱳ ᱯᱞᱮᱭᱚᱯ ᱨᱮ ᱵᱞᱮᱠ ᱥᱠᱨᱤᱱ ᱟᱨᱵᱟᱝ ᱠᱷᱟᱹᱞᱤ ᱥᱴᱮᱴᱞᱤᱝ ᱮᱢ ᱧᱟᱢᱟ ᱮᱱᱠᱷᱟᱱ ᱢᱤᱰᱤᱭᱟ ᱴᱩᱱᱮᱞᱤᱝ ᱵᱚᱫᱚᱞ ᱢᱮ ᱾</string>
<string name="show_image_indicators_title">ᱪᱤᱛᱟᱹᱨ ᱪᱤᱱᱦᱟᱹ ᱠᱚ ᱧᱮᱞ ᱢᱮ</string>
<string name="check_new_streams">ᱱᱟᱣᱟ ᱥᱴᱨᱤᱢ ᱞᱟᱹᱜᱤᱫ ᱪᱟᱪᱞᱟᱣ ᱢᱮ</string>
<string name="create_error_notification">ᱢᱤᱫ error notification ᱛᱮᱭᱟᱨ ᱢᱮ</string>
<string name="subscriptions_export_unsuccessful">ᱥᱮᱞᱮᱫ ᱮᱠᱥᱯᱳᱨᱴ ᱵᱟᱝ ᱦᱩᱭ ᱫᱟᱲᱮᱭᱟᱜ ᱠᱟᱱᱟ</string>

View File

@ -634,8 +634,6 @@
<string name="comments_are_disabled">Sos cummentos sunt disabilitados</string>
<string name="detail_heart_img_view_description">Su creadore b\'at postu unu coro</string>
<string name="mark_as_watched">Marca comente pompiadu</string>
<string name="show_image_indicators_summary">Ammustra sos listrones colorados de Picasso in subra de sas immàgines chi indicant sa fonte issoro: ruja pro sa retze, biaita pro su discu e birde pro sa memòria</string>
<string name="show_image_indicators_title">Ammustra sos indicadores de immàgines</string>
<string name="remote_search_suggestions">Impòsitos de chirca remota</string>
<string name="local_search_suggestions">Impòsitos de chirca locales</string>
<plurals name="deleted_downloads_toast">

View File

@ -653,8 +653,6 @@
<string name="low_quality_smaller">Nízka kvalita (menšie)</string>
<string name="high_quality_larger">Vysoká kvalita (väčšie)</string>
<string name="seekbar_preview_thumbnail_title">Náhľad miniatúry pri vyhľadávaní</string>
<string name="show_image_indicators_summary">Zobrazí farebné pásiky Picasso na obrázkoch podľa ich zdroja: červený pre sieť, modrý pre disk a zelený pre pamäť</string>
<string name="show_image_indicators_title">Zobraziť indikátory obrázka</string>
<string name="main_page_content_swipe_remove">Potiahnutím vymazať</string>
<string name="comments_are_disabled">Komentáre sú zakázané</string>
<string name="start_main_player_fullscreen_summary">Ak je automatické otáčanie zablokované, nespustí videá v miniprehrávači, ale prepne sa do celoobrazovkového režimu. Do miniprehrávača sa dostanete po ukončení režimu celej obrazovky</string>

View File

@ -633,8 +633,6 @@
<string name="comments_are_disabled">Fallooyinka waa laxidhay</string>
<string name="detail_heart_img_view_description">Kahelay soosaaraha</string>
<string name="mark_as_watched">Waan daawaday</string>
<string name="show_image_indicators_summary">Soo bandhig shaambado midabka Picasso leh sawirrada dushooda oo tilmaamaya isha laga keenay: guduud waa khadka, buluug waa kaydka gudaha, cagaar waa kaydka K/G</string>
<string name="show_image_indicators_title">Tus tilmaamayaasha sawirka</string>
<string name="remote_search_suggestions">Soojeedinada raadinta banaanka</string>
<string name="local_search_suggestions">Soojeedinada raadinta gudaha</string>
<string name="progressive_load_interval_title">Cabirka soodaarida udhexeeya</string>

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