Compare commits

..

4 Commits

Author SHA1 Message Date
Profpatsch
e20122958d newpipe/error: declare variables from onCreate as lateinit
We will init them at the beginning of the view lifecycle, so they are
definitly not null.
2024-03-30 14:17:22 +01:00
Profpatsch
726cdebcd3 newpipe/error: fix warnings after kotlin conversion 2024-03-30 13:16:23 +01:00
Profpatsch
78de1a0bed Delete unused class AcraReportSenderFactory 2024-03-30 12:51:14 +01:00
Profpatsch
4a3e316dd0 Auto-migrate newpipe/error to kotlin
Only error was a nullable check in AcraReportSender, which I fixed by
replacing null with an empty array.

I tested the error report by producing a network error and opening the
report, creating an email and accepting the EULA.

Warnings will be fixed in the next commit.
2024-03-30 12:50:38 +01:00
167 changed files with 1470 additions and 4809 deletions

View File

@@ -20,8 +20,8 @@ android {
resValue "string", "app_name", "NewPipe"
minSdk 21
targetSdk 33
versionCode 997
versionName "0.27.0"
versionCode 996
versionName "0.26.1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@@ -198,7 +198,7 @@ dependencies {
// name and the commit hash with the commit hash of the (pushed) commit you want to test
// This works thanks to JitPack: https://jitpack.io/
implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751'
implementation 'com.github.TeamNewPipe:NewPipeExtractor:v0.24.0'
implementation 'com.github.Stypox:NewPipeExtractor:aaf3231fc75d7b4177549fec4aa7e672bfe84015'
implementation 'com.github.TeamNewPipe:NoNonsense-FilePicker:5.0.0'
/** Checkstyle **/

View File

@@ -1,730 +0,0 @@
{
"formatVersion": 1,
"database": {
"version": 9,
"identityHash": "7591e8039faa74d8c0517dc867af9d3e",
"entities": [
{
"tableName": "subscriptions",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT, `name` TEXT, `avatar_url` TEXT, `subscriber_count` INTEGER, `description` TEXT, `notification_mode` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "uid",
"columnName": "uid",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "serviceId",
"columnName": "service_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "avatarUrl",
"columnName": "avatar_url",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "subscriberCount",
"columnName": "subscriber_count",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "description",
"columnName": "description",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "notificationMode",
"columnName": "notification_mode",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"uid"
]
},
"indices": [
{
"name": "index_subscriptions_service_id_url",
"unique": true,
"columnNames": [
"service_id",
"url"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_subscriptions_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
}
],
"foreignKeys": []
},
{
"tableName": "search_history",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`creation_date` INTEGER, `service_id` INTEGER NOT NULL, `search` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)",
"fields": [
{
"fieldPath": "creationDate",
"columnName": "creation_date",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "serviceId",
"columnName": "service_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "search",
"columnName": "search",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_search_history_search",
"unique": false,
"columnNames": [
"search"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_search_history_search` ON `${TABLE_NAME}` (`search`)"
}
],
"foreignKeys": []
},
{
"tableName": "streams",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT NOT NULL, `title` TEXT NOT NULL, `stream_type` TEXT NOT NULL, `duration` INTEGER NOT NULL, `uploader` TEXT NOT NULL, `uploader_url` TEXT, `thumbnail_url` TEXT, `view_count` INTEGER, `textual_upload_date` TEXT, `upload_date` INTEGER, `is_upload_date_approximation` INTEGER)",
"fields": [
{
"fieldPath": "uid",
"columnName": "uid",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "serviceId",
"columnName": "service_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "streamType",
"columnName": "stream_type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "duration",
"columnName": "duration",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "uploader",
"columnName": "uploader",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "uploaderUrl",
"columnName": "uploader_url",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "thumbnailUrl",
"columnName": "thumbnail_url",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "viewCount",
"columnName": "view_count",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "textualUploadDate",
"columnName": "textual_upload_date",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploadDate",
"columnName": "upload_date",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "isUploadDateApproximation",
"columnName": "is_upload_date_approximation",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"uid"
]
},
"indices": [
{
"name": "index_streams_service_id_url",
"unique": true,
"columnNames": [
"service_id",
"url"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_streams_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
}
],
"foreignKeys": []
},
{
"tableName": "stream_history",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `access_date` INTEGER NOT NULL, `repeat_count` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `access_date`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "streamUid",
"columnName": "stream_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "accessDate",
"columnName": "access_date",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "repeatCount",
"columnName": "repeat_count",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"stream_id",
"access_date"
]
},
"indices": [
{
"name": "index_stream_history_stream_id",
"unique": false,
"columnNames": [
"stream_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_stream_history_stream_id` ON `${TABLE_NAME}` (`stream_id`)"
}
],
"foreignKeys": [
{
"table": "streams",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"stream_id"
],
"referencedColumns": [
"uid"
]
}
]
},
{
"tableName": "stream_state",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `progress_time` INTEGER NOT NULL, PRIMARY KEY(`stream_id`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "streamUid",
"columnName": "stream_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "progressMillis",
"columnName": "progress_time",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"stream_id"
]
},
"indices": [],
"foreignKeys": [
{
"table": "streams",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"stream_id"
],
"referencedColumns": [
"uid"
]
}
]
},
{
"tableName": "playlists",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `is_thumbnail_permanent` INTEGER NOT NULL, `thumbnail_stream_id` INTEGER NOT NULL, `display_index` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "uid",
"columnName": "uid",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isThumbnailPermanent",
"columnName": "is_thumbnail_permanent",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "thumbnailStreamId",
"columnName": "thumbnail_stream_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "displayIndex",
"columnName": "display_index",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"uid"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "playlist_stream_join",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`playlist_id` INTEGER NOT NULL, `stream_id` INTEGER NOT NULL, `join_index` INTEGER NOT NULL, PRIMARY KEY(`playlist_id`, `join_index`), FOREIGN KEY(`playlist_id`) REFERENCES `playlists`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
"fields": [
{
"fieldPath": "playlistUid",
"columnName": "playlist_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "streamUid",
"columnName": "stream_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "index",
"columnName": "join_index",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"playlist_id",
"join_index"
]
},
"indices": [
{
"name": "index_playlist_stream_join_playlist_id_join_index",
"unique": true,
"columnNames": [
"playlist_id",
"join_index"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_playlist_stream_join_playlist_id_join_index` ON `${TABLE_NAME}` (`playlist_id`, `join_index`)"
},
{
"name": "index_playlist_stream_join_stream_id",
"unique": false,
"columnNames": [
"stream_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_playlist_stream_join_stream_id` ON `${TABLE_NAME}` (`stream_id`)"
}
],
"foreignKeys": [
{
"table": "playlists",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"playlist_id"
],
"referencedColumns": [
"uid"
]
},
{
"table": "streams",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"stream_id"
],
"referencedColumns": [
"uid"
]
}
]
},
{
"tableName": "remote_playlists",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, `thumbnail_url` TEXT, `uploader` TEXT, `display_index` INTEGER NOT NULL, `stream_count` INTEGER)",
"fields": [
{
"fieldPath": "uid",
"columnName": "uid",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "serviceId",
"columnName": "service_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "thumbnailUrl",
"columnName": "thumbnail_url",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploader",
"columnName": "uploader",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "displayIndex",
"columnName": "display_index",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "streamCount",
"columnName": "stream_count",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"uid"
]
},
"indices": [
{
"name": "index_remote_playlists_service_id_url",
"unique": true,
"columnNames": [
"service_id",
"url"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_remote_playlists_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
}
],
"foreignKeys": []
},
{
"tableName": "feed",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `subscription_id` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `subscription_id`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`subscription_id`) REFERENCES `subscriptions`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
"fields": [
{
"fieldPath": "streamId",
"columnName": "stream_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "subscriptionId",
"columnName": "subscription_id",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"stream_id",
"subscription_id"
]
},
"indices": [
{
"name": "index_feed_subscription_id",
"unique": false,
"columnNames": [
"subscription_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_feed_subscription_id` ON `${TABLE_NAME}` (`subscription_id`)"
}
],
"foreignKeys": [
{
"table": "streams",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"stream_id"
],
"referencedColumns": [
"uid"
]
},
{
"table": "subscriptions",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"subscription_id"
],
"referencedColumns": [
"uid"
]
}
]
},
{
"tableName": "feed_group",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `icon_id` INTEGER NOT NULL, `sort_order` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "uid",
"columnName": "uid",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "icon",
"columnName": "icon_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "sortOrder",
"columnName": "sort_order",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"uid"
]
},
"indices": [
{
"name": "index_feed_group_sort_order",
"unique": false,
"columnNames": [
"sort_order"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_feed_group_sort_order` ON `${TABLE_NAME}` (`sort_order`)"
}
],
"foreignKeys": []
},
{
"tableName": "feed_group_subscription_join",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`group_id` INTEGER NOT NULL, `subscription_id` INTEGER NOT NULL, PRIMARY KEY(`group_id`, `subscription_id`), FOREIGN KEY(`group_id`) REFERENCES `feed_group`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`subscription_id`) REFERENCES `subscriptions`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
"fields": [
{
"fieldPath": "feedGroupId",
"columnName": "group_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "subscriptionId",
"columnName": "subscription_id",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"group_id",
"subscription_id"
]
},
"indices": [
{
"name": "index_feed_group_subscription_join_subscription_id",
"unique": false,
"columnNames": [
"subscription_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_feed_group_subscription_join_subscription_id` ON `${TABLE_NAME}` (`subscription_id`)"
}
],
"foreignKeys": [
{
"table": "feed_group",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"group_id"
],
"referencedColumns": [
"uid"
]
},
{
"table": "subscriptions",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"subscription_id"
],
"referencedColumns": [
"uid"
]
}
]
},
{
"tableName": "feed_last_updated",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`subscription_id` INTEGER NOT NULL, `last_updated` INTEGER, PRIMARY KEY(`subscription_id`), FOREIGN KEY(`subscription_id`) REFERENCES `subscriptions`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
"fields": [
{
"fieldPath": "subscriptionId",
"columnName": "subscription_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastUpdated",
"columnName": "last_updated",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"subscription_id"
]
},
"indices": [],
"foreignKeys": [
{
"table": "subscriptions",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"subscription_id"
],
"referencedColumns": [
"uid"
]
}
]
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '7591e8039faa74d8c0517dc867af9d3e')"
]
}
}

View File

@@ -13,8 +13,6 @@ import org.junit.Assert.assertNull
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.schabi.newpipe.database.playlist.model.PlaylistEntity
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity
import org.schabi.newpipe.extractor.ServiceList
import org.schabi.newpipe.extractor.stream.StreamType
@@ -24,17 +22,13 @@ class DatabaseMigrationTest {
private const val DEFAULT_SERVICE_ID = 0
private const val DEFAULT_URL = "https://www.youtube.com/watch?v=cDphUib5iG4"
private const val DEFAULT_TITLE = "Test Title"
private const val DEFAULT_NAME = "Test Name"
private val DEFAULT_TYPE = StreamType.VIDEO_STREAM
private const val DEFAULT_DURATION = 480L
private const val DEFAULT_UPLOADER_NAME = "Uploader Test"
private const val DEFAULT_THUMBNAIL = "https://example.com/example.jpg"
private const val DEFAULT_SECOND_SERVICE_ID = 1
private const val DEFAULT_SECOND_SERVICE_ID = 0
private const val DEFAULT_SECOND_URL = "https://www.youtube.com/watch?v=ncQU6iBn5Fc"
private const val DEFAULT_THIRD_SERVICE_ID = 2
private const val DEFAULT_THIRD_URL = "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
}
@get:Rule
@@ -121,13 +115,6 @@ class DatabaseMigrationTest {
Migrations.MIGRATION_7_8
)
testHelper.runMigrationsAndValidate(
AppDatabase.DATABASE_NAME,
Migrations.DB_VER_9,
true,
Migrations.MIGRATION_8_9
)
val migratedDatabaseV3 = getMigratedDatabase()
val listFromDB = migratedDatabaseV3.streamDAO().all.blockingFirst()
@@ -211,11 +198,6 @@ class DatabaseMigrationTest {
true, Migrations.MIGRATION_7_8
)
testHelper.runMigrationsAndValidate(
AppDatabase.DATABASE_NAME, Migrations.DB_VER_9,
true, Migrations.MIGRATION_8_9
)
val migratedDatabaseV8 = getMigratedDatabase()
val listFromDB = migratedDatabaseV8.searchHistoryDAO().all.blockingFirst()
@@ -225,94 +207,6 @@ class DatabaseMigrationTest {
assertNotEquals(listFromDB[0].serviceId, listFromDB[1].serviceId)
}
@Test
fun migrateDatabaseFrom8to9() {
val databaseInV8 = testHelper.createDatabase(AppDatabase.DATABASE_NAME, Migrations.DB_VER_8)
val localUid1: Long
val localUid2: Long
val remoteUid1: Long
val remoteUid2: Long
databaseInV8.run {
localUid1 = insert(
"playlists", SQLiteDatabase.CONFLICT_FAIL,
ContentValues().apply {
put("name", DEFAULT_NAME + "1")
put("is_thumbnail_permanent", false)
put("thumbnail_stream_id", -1)
}
)
localUid2 = insert(
"playlists", SQLiteDatabase.CONFLICT_FAIL,
ContentValues().apply {
put("name", DEFAULT_NAME + "2")
put("is_thumbnail_permanent", false)
put("thumbnail_stream_id", -1)
}
)
delete(
"playlists", "uid = ?",
Array(1) { localUid1 }
)
remoteUid1 = insert(
"remote_playlists", SQLiteDatabase.CONFLICT_FAIL,
ContentValues().apply {
put("service_id", DEFAULT_SERVICE_ID)
put("url", DEFAULT_URL)
}
)
remoteUid2 = insert(
"remote_playlists", SQLiteDatabase.CONFLICT_FAIL,
ContentValues().apply {
put("service_id", DEFAULT_SECOND_SERVICE_ID)
put("url", DEFAULT_SECOND_URL)
}
)
delete(
"remote_playlists", "uid = ?",
Array(1) { remoteUid2 }
)
close()
}
testHelper.runMigrationsAndValidate(
AppDatabase.DATABASE_NAME,
Migrations.DB_VER_9,
true,
Migrations.MIGRATION_8_9
)
val migratedDatabaseV9 = getMigratedDatabase()
var localListFromDB = migratedDatabaseV9.playlistDAO().all.blockingFirst()
var remoteListFromDB = migratedDatabaseV9.playlistRemoteDAO().all.blockingFirst()
assertEquals(1, localListFromDB.size)
assertEquals(localUid2, localListFromDB[0].uid)
assertEquals(-1, localListFromDB[0].displayIndex)
assertEquals(1, remoteListFromDB.size)
assertEquals(remoteUid1, remoteListFromDB[0].uid)
assertEquals(-1, remoteListFromDB[0].displayIndex)
val localUid3 = migratedDatabaseV9.playlistDAO().insert(
PlaylistEntity(DEFAULT_NAME + "3", false, -1, -1)
)
val remoteUid3 = migratedDatabaseV9.playlistRemoteDAO().insert(
PlaylistRemoteEntity(
DEFAULT_THIRD_SERVICE_ID, DEFAULT_NAME, DEFAULT_THIRD_URL,
DEFAULT_THUMBNAIL, DEFAULT_UPLOADER_NAME, -1, 10
)
)
localListFromDB = migratedDatabaseV9.playlistDAO().all.blockingFirst()
remoteListFromDB = migratedDatabaseV9.playlistRemoteDAO().all.blockingFirst()
assertEquals(2, localListFromDB.size)
assertEquals(localUid3, localListFromDB[1].uid)
assertEquals(-1, localListFromDB[1].displayIndex)
assertEquals(2, remoteListFromDB.size)
assertEquals(remoteUid3, remoteListFromDB[1].uid)
assertEquals(-1, remoteListFromDB[1].displayIndex)
}
private fun getMigratedDatabase(): AppDatabase {
val database: AppDatabase = Room.databaseBuilder(
ApplicationProvider.getApplicationContext(),

View File

@@ -8,7 +8,6 @@ import static org.schabi.newpipe.database.Migrations.MIGRATION_4_5;
import static org.schabi.newpipe.database.Migrations.MIGRATION_5_6;
import static org.schabi.newpipe.database.Migrations.MIGRATION_6_7;
import static org.schabi.newpipe.database.Migrations.MIGRATION_7_8;
import static org.schabi.newpipe.database.Migrations.MIGRATION_8_9;
import android.content.Context;
import android.database.Cursor;
@@ -29,7 +28,7 @@ public final class NewPipeDatabase {
return Room
.databaseBuilder(context.getApplicationContext(), AppDatabase.class, DATABASE_NAME)
.addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5,
MIGRATION_5_6, MIGRATION_6_7, MIGRATION_7_8, MIGRATION_8_9)
MIGRATION_5_6, MIGRATION_6_7, MIGRATION_7_8)
.build();
}

View File

@@ -1,6 +1,6 @@
package org.schabi.newpipe.database;
import static org.schabi.newpipe.database.Migrations.DB_VER_9;
import static org.schabi.newpipe.database.Migrations.DB_VER_8;
import androidx.room.Database;
import androidx.room.RoomDatabase;
@@ -38,7 +38,7 @@ import org.schabi.newpipe.database.subscription.SubscriptionEntity;
FeedEntity.class, FeedGroupEntity.class, FeedGroupSubscriptionEntity.class,
FeedLastUpdatedEntity.class
},
version = DB_VER_9
version = DB_VER_8
)
public abstract class AppDatabase extends RoomDatabase {
public static final String DATABASE_NAME = "newpipe.db";

View File

@@ -26,7 +26,6 @@ public final class Migrations {
public static final int DB_VER_6 = 6;
public static final int DB_VER_7 = 7;
public static final int DB_VER_8 = 8;
public static final int DB_VER_9 = 9;
private static final String TAG = Migrations.class.getName();
public static final boolean DEBUG = MainActivity.DEBUG;
@@ -188,7 +187,7 @@ public final class Migrations {
@Override
public void migrate(@NonNull final SupportSQLiteDatabase database) {
database.execSQL("ALTER TABLE `subscriptions` ADD COLUMN `notification_mode` "
+ "INTEGER NOT NULL DEFAULT 0");
+ "INTEGER NOT NULL DEFAULT 0");
}
};
@@ -246,62 +245,6 @@ public final class Migrations {
}
};
public static final Migration MIGRATION_8_9 = new Migration(DB_VER_8, DB_VER_9) {
@Override
public void migrate(@NonNull final SupportSQLiteDatabase database) {
try {
database.beginTransaction();
// Update playlists.
// Create a temp table to initialize display_index.
database.execSQL("CREATE TABLE `playlists_tmp` "
+ "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "
+ "`name` TEXT, `is_thumbnail_permanent` INTEGER NOT NULL, "
+ "`thumbnail_stream_id` INTEGER NOT NULL, "
+ "`display_index` INTEGER NOT NULL)");
database.execSQL("INSERT INTO `playlists_tmp` "
+ "(`uid`, `name`, `is_thumbnail_permanent`, `thumbnail_stream_id`, "
+ "`display_index`) "
+ "SELECT `uid`, `name`, `is_thumbnail_permanent`, `thumbnail_stream_id`, "
+ "-1 "
+ "FROM `playlists`");
// Replace the old table, note that this also removes the index on the name which
// we don't need anymore.
database.execSQL("DROP TABLE `playlists`");
database.execSQL("ALTER TABLE `playlists_tmp` RENAME TO `playlists`");
// Update remote_playlists.
// Create a temp table to initialize display_index.
database.execSQL("CREATE TABLE `remote_playlists_tmp` "
+ "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "
+ "`service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, "
+ "`thumbnail_url` TEXT, `uploader` TEXT, "
+ "`display_index` INTEGER NOT NULL,"
+ "`stream_count` INTEGER)");
database.execSQL("INSERT INTO `remote_playlists_tmp` (`uid`, `service_id`, "
+ "`name`, `url`, `thumbnail_url`, `uploader`, `display_index`, "
+ "`stream_count`)"
+ "SELECT `uid`, `service_id`, `name`, `url`, `thumbnail_url`, `uploader`, "
+ "-1, `stream_count` FROM `remote_playlists`");
// Replace the old table, note that this also removes the index on the name which
// we don't need anymore.
database.execSQL("DROP TABLE `remote_playlists`");
database.execSQL("ALTER TABLE `remote_playlists_tmp` RENAME TO `remote_playlists`");
// Create index on the new table.
database.execSQL("CREATE UNIQUE INDEX `index_remote_playlists_service_id_url` "
+ "ON `remote_playlists` (`service_id`, `url`)");
database.setTransactionSuccessful();
} finally {
database.endTransaction();
}
}
};
private Migrations() {
}
}

View File

@@ -13,17 +13,12 @@ public class PlaylistDuplicatesEntry extends PlaylistMetadataEntry {
@ColumnInfo(name = PLAYLIST_TIMES_STREAM_IS_CONTAINED)
public final long timesStreamIsContained;
@SuppressWarnings("checkstyle:ParameterNumber")
public PlaylistDuplicatesEntry(final long uid,
final String name,
final String thumbnailUrl,
final boolean isThumbnailPermanent,
final long thumbnailStreamId,
final long displayIndex,
final long streamCount,
final long timesStreamIsContained) {
super(uid, name, thumbnailUrl, isThumbnailPermanent, thumbnailStreamId, displayIndex,
streamCount);
super(uid, name, thumbnailUrl, streamCount);
this.timesStreamIsContained = timesStreamIsContained;
}
}

View File

@@ -1,13 +1,22 @@
package org.schabi.newpipe.database.playlist;
import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public interface PlaylistLocalItem extends LocalItem {
String getOrderingName();
long getDisplayIndex();
long getUid();
void setDisplayIndex(long displayIndex);
static List<PlaylistLocalItem> merge(
final List<PlaylistMetadataEntry> localPlaylists,
final List<PlaylistRemoteEntity> remotePlaylists) {
return Stream.concat(localPlaylists.stream(), remotePlaylists.stream())
.sorted(Comparator.comparing(PlaylistLocalItem::getOrderingName,
Comparator.nullsLast(String.CASE_INSENSITIVE_ORDER)))
.collect(Collectors.toList());
}
}

View File

@@ -2,40 +2,27 @@ package org.schabi.newpipe.database.playlist;
import androidx.room.ColumnInfo;
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_DISPLAY_INDEX;
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_ID;
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_NAME;
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_PERMANENT;
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_STREAM_ID;
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_URL;
public class PlaylistMetadataEntry implements PlaylistLocalItem {
public static final String PLAYLIST_STREAM_COUNT = "streamCount";
@ColumnInfo(name = PLAYLIST_ID)
private final long uid;
public final long uid;
@ColumnInfo(name = PLAYLIST_NAME)
public final String name;
@ColumnInfo(name = PLAYLIST_THUMBNAIL_PERMANENT)
private final boolean isThumbnailPermanent;
@ColumnInfo(name = PLAYLIST_THUMBNAIL_STREAM_ID)
private final long thumbnailStreamId;
@ColumnInfo(name = PLAYLIST_THUMBNAIL_URL)
public final String thumbnailUrl;
@ColumnInfo(name = PLAYLIST_DISPLAY_INDEX)
private long displayIndex;
@ColumnInfo(name = PLAYLIST_STREAM_COUNT)
public final long streamCount;
public PlaylistMetadataEntry(final long uid, final String name, final String thumbnailUrl,
final boolean isThumbnailPermanent, final long thumbnailStreamId,
final long displayIndex, final long streamCount) {
final long streamCount) {
this.uid = uid;
this.name = name;
this.thumbnailUrl = thumbnailUrl;
this.isThumbnailPermanent = isThumbnailPermanent;
this.thumbnailStreamId = thumbnailStreamId;
this.displayIndex = displayIndex;
this.streamCount = streamCount;
}
@@ -48,27 +35,4 @@ public class PlaylistMetadataEntry implements PlaylistLocalItem {
public String getOrderingName() {
return name;
}
public boolean isThumbnailPermanent() {
return isThumbnailPermanent;
}
public long getThumbnailStreamId() {
return thumbnailStreamId;
}
@Override
public long getDisplayIndex() {
return displayIndex;
}
@Override
public long getUid() {
return uid;
}
@Override
public void setDisplayIndex(final long displayIndex) {
this.displayIndex = displayIndex;
}
}

View File

@@ -2,7 +2,6 @@ package org.schabi.newpipe.database.playlist.dao;
import androidx.room.Dao;
import androidx.room.Query;
import androidx.room.Transaction;
import org.schabi.newpipe.database.BasicDAO;
import org.schabi.newpipe.database.playlist.model.PlaylistEntity;
@@ -37,17 +36,4 @@ public interface PlaylistDAO extends BasicDAO<PlaylistEntity> {
@Query("SELECT COUNT(*) FROM " + PLAYLIST_TABLE)
Flowable<Long> getCount();
@Transaction
default long upsertPlaylist(final PlaylistEntity playlist) {
final long playlistId = playlist.getUid();
if (playlistId == -1) {
// This situation is probably impossible.
return insert(playlist);
} else {
update(playlist);
return playlistId;
}
}
}

View File

@@ -11,7 +11,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;
@@ -32,18 +31,10 @@ public interface PlaylistRemoteDAO extends BasicDAO<PlaylistRemoteEntity> {
+ " WHERE " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId")
Flowable<List<PlaylistRemoteEntity>> listByService(int serviceId);
@Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE + " WHERE "
+ REMOTE_PLAYLIST_ID + " = :playlistId")
Flowable<List<PlaylistRemoteEntity>> getPlaylist(long playlistId);
@Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE + " WHERE "
+ REMOTE_PLAYLIST_URL + " = :url AND " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId")
Flowable<List<PlaylistRemoteEntity>> getPlaylist(long serviceId, String url);
@Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE
+ " ORDER BY " + REMOTE_PLAYLIST_DISPLAY_INDEX)
Flowable<List<PlaylistRemoteEntity>> getPlaylists();
@Query("SELECT " + REMOTE_PLAYLIST_ID + " FROM " + REMOTE_PLAYLIST_TABLE
+ " WHERE " + REMOTE_PLAYLIST_URL + " = :url "
+ "AND " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId")

View File

@@ -18,12 +18,10 @@ import io.reactivex.rxjava3.core.Flowable;
import static org.schabi.newpipe.database.playlist.PlaylistDuplicatesEntry.PLAYLIST_TIMES_STREAM_IS_CONTAINED;
import static org.schabi.newpipe.database.playlist.PlaylistMetadataEntry.PLAYLIST_STREAM_COUNT;
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_DISPLAY_INDEX;
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.DEFAULT_THUMBNAIL;
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_ID;
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_NAME;
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_TABLE;
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_PERMANENT;
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_STREAM_ID;
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_URL;
import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JOIN_INDEX;
@@ -93,9 +91,7 @@ public interface PlaylistStreamDAO extends BasicDAO<PlaylistStreamEntity> {
Flowable<List<PlaylistStreamEntry>> getOrderedStreamsOf(long playlistId);
@Transaction
@Query("SELECT " + PLAYLIST_ID + ", " + PLAYLIST_NAME + ", "
+ PLAYLIST_THUMBNAIL_PERMANENT + ", " + PLAYLIST_THUMBNAIL_STREAM_ID + ", "
+ PLAYLIST_DISPLAY_INDEX + ", "
@Query("SELECT " + PLAYLIST_ID + ", " + PLAYLIST_NAME + ","
+ " CASE WHEN " + PLAYLIST_THUMBNAIL_STREAM_ID + " = "
+ PlaylistEntity.DEFAULT_THUMBNAIL_ID + " THEN " + "'" + DEFAULT_THUMBNAIL + "'"
@@ -109,7 +105,7 @@ public interface PlaylistStreamDAO extends BasicDAO<PlaylistStreamEntity> {
+ " LEFT JOIN " + PLAYLIST_STREAM_JOIN_TABLE
+ " ON " + PLAYLIST_TABLE + "." + PLAYLIST_ID + " = " + JOIN_PLAYLIST_ID
+ " GROUP BY " + PLAYLIST_ID
+ " ORDER BY " + PLAYLIST_DISPLAY_INDEX)
+ " ORDER BY " + PLAYLIST_NAME + " COLLATE NOCASE ASC")
Flowable<List<PlaylistMetadataEntry>> getPlaylistMetadata();
@RewriteQueriesToDropUnusedColumns
@@ -130,9 +126,8 @@ public interface PlaylistStreamDAO extends BasicDAO<PlaylistStreamEntity> {
Flowable<List<PlaylistStreamEntry>> getStreamsWithoutDuplicates(long playlistId);
@Transaction
@Query("SELECT " + PLAYLIST_TABLE + "." + PLAYLIST_ID + ", " + PLAYLIST_NAME + ", "
+ PLAYLIST_THUMBNAIL_PERMANENT + ", " + PLAYLIST_THUMBNAIL_STREAM_ID + ", "
+ PLAYLIST_DISPLAY_INDEX + ", "
@Query("SELECT " + PLAYLIST_TABLE + "." + PLAYLIST_ID + ", "
+ PLAYLIST_NAME + ", "
+ " CASE WHEN " + PLAYLIST_THUMBNAIL_STREAM_ID + " = "
+ PlaylistEntity.DEFAULT_THUMBNAIL_ID + " THEN " + "'" + DEFAULT_THUMBNAIL + "'"
@@ -154,6 +149,6 @@ public interface PlaylistStreamDAO extends BasicDAO<PlaylistStreamEntity> {
+ " AND :streamUrl = :streamUrl"
+ " GROUP BY " + JOIN_PLAYLIST_ID
+ " ORDER BY " + PLAYLIST_DISPLAY_INDEX)
+ " ORDER BY " + PLAYLIST_NAME + " COLLATE NOCASE ASC")
Flowable<List<PlaylistDuplicatesEntry>> getPlaylistDuplicatesMetadata(String streamUrl);
}

View File

@@ -2,15 +2,16 @@ package org.schabi.newpipe.database.playlist.model;
import androidx.room.ColumnInfo;
import androidx.room.Entity;
import androidx.room.Ignore;
import androidx.room.Index;
import androidx.room.PrimaryKey;
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_NAME;
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_TABLE;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
@Entity(tableName = PLAYLIST_TABLE)
@Entity(tableName = PLAYLIST_TABLE,
indices = {@Index(value = {PLAYLIST_NAME})})
public class PlaylistEntity {
public static final String DEFAULT_THUMBNAIL = "drawable://"
@@ -21,7 +22,6 @@ public class PlaylistEntity {
public static final String PLAYLIST_ID = "uid";
public static final String PLAYLIST_NAME = "name";
public static final String PLAYLIST_THUMBNAIL_URL = "thumbnail_url";
public static final String PLAYLIST_DISPLAY_INDEX = "display_index";
public static final String PLAYLIST_THUMBNAIL_PERMANENT = "is_thumbnail_permanent";
public static final String PLAYLIST_THUMBNAIL_STREAM_ID = "thumbnail_stream_id";
@@ -38,24 +38,11 @@ public class PlaylistEntity {
@ColumnInfo(name = PLAYLIST_THUMBNAIL_STREAM_ID)
private long thumbnailStreamId;
@ColumnInfo(name = PLAYLIST_DISPLAY_INDEX)
private long displayIndex;
public PlaylistEntity(final String name, final boolean isThumbnailPermanent,
final long thumbnailStreamId, final long displayIndex) {
final long thumbnailStreamId) {
this.name = name;
this.isThumbnailPermanent = isThumbnailPermanent;
this.thumbnailStreamId = thumbnailStreamId;
this.displayIndex = displayIndex;
}
@Ignore
public PlaylistEntity(final PlaylistMetadataEntry item) {
this.uid = item.getUid();
this.name = item.name;
this.isThumbnailPermanent = item.isThumbnailPermanent();
this.thumbnailStreamId = item.getThumbnailStreamId();
this.displayIndex = item.getDisplayIndex();
}
public long getUid() {
@@ -90,11 +77,4 @@ public class PlaylistEntity {
this.isThumbnailPermanent = isThumbnailSet;
}
public long getDisplayIndex() {
return displayIndex;
}
public void setDisplayIndex(final long displayIndex) {
this.displayIndex = displayIndex;
}
}

View File

@@ -21,6 +21,7 @@ import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.RE
@Entity(tableName = REMOTE_PLAYLIST_TABLE,
indices = {
@Index(value = {REMOTE_PLAYLIST_NAME}),
@Index(value = {REMOTE_PLAYLIST_SERVICE_ID, REMOTE_PLAYLIST_URL}, unique = true)
})
public class PlaylistRemoteEntity implements PlaylistLocalItem {
@@ -31,7 +32,6 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem {
public static final String REMOTE_PLAYLIST_URL = "url";
public static final String REMOTE_PLAYLIST_THUMBNAIL_URL = "thumbnail_url";
public static final String REMOTE_PLAYLIST_UPLOADER_NAME = "uploader";
public static final String REMOTE_PLAYLIST_DISPLAY_INDEX = "display_index";
public static final String REMOTE_PLAYLIST_STREAM_COUNT = "stream_count";
@PrimaryKey(autoGenerate = true)
@@ -53,9 +53,6 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem {
@ColumnInfo(name = REMOTE_PLAYLIST_UPLOADER_NAME)
private String uploader;
@ColumnInfo(name = REMOTE_PLAYLIST_DISPLAY_INDEX)
private long displayIndex = -1; // Make sure the new item is on the top
@ColumnInfo(name = REMOTE_PLAYLIST_STREAM_COUNT)
private Long streamCount;
@@ -70,19 +67,6 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem {
this.streamCount = streamCount;
}
@Ignore
public PlaylistRemoteEntity(final int serviceId, final String name, final String url,
final String thumbnailUrl, final String uploader,
final long displayIndex, final Long streamCount) {
this.serviceId = serviceId;
this.name = name;
this.url = url;
this.thumbnailUrl = thumbnailUrl;
this.uploader = uploader;
this.displayIndex = displayIndex;
this.streamCount = streamCount;
}
@Ignore
public PlaylistRemoteEntity(final PlaylistInfo info) {
this(info.getServiceId(), info.getName(), info.getUrl(),
@@ -109,7 +93,6 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem {
&& TextUtils.equals(getUploader(), info.getUploaderName());
}
@Override
public long getUid() {
return uid;
}
@@ -158,16 +141,6 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem {
this.uploader = uploader;
}
@Override
public long getDisplayIndex() {
return displayIndex;
}
@Override
public void setDisplayIndex(final long displayIndex) {
this.displayIndex = displayIndex;
}
public Long getStreamCount() {
return streamCount;
}

View File

@@ -1,7 +1,6 @@
package org.schabi.newpipe.database.subscription;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.room.ColumnInfo;
import androidx.room.Entity;
import androidx.room.Ignore;
@@ -96,12 +95,11 @@ public class SubscriptionEntity {
this.name = name;
}
@Nullable
public String getAvatarUrl() {
return avatarUrl;
}
public void setAvatarUrl(@Nullable final String avatarUrl) {
public void setAvatarUrl(final String avatarUrl) {
this.avatarUrl = avatarUrl;
}

View File

@@ -859,19 +859,20 @@ public class DownloadDialog extends DialogFragment
return;
}
// Check for free storage space
final long freeSpace = mainStorage.getFreeStorageSpace();
if (freeSpace <= size) {
Toast.makeText(context, getString(R.
string.error_insufficient_storage), Toast.LENGTH_LONG).show();
// move the user to storage setting tab
final Intent storageSettingsIntent = new Intent(Settings.
ACTION_INTERNAL_STORAGE_SETTINGS);
if (storageSettingsIntent.resolveActivity(context.getPackageManager())
!= null) {
startActivity(storageSettingsIntent);
// Check for free memory space (for api 24 and up)
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
final long freeSpace = mainStorage.getFreeMemory();
if (freeSpace <= size) {
Toast.makeText(context, getString(R.
string.error_insufficient_storage), Toast.LENGTH_LONG).show();
// move the user to storage setting tab
final Intent storageSettingsIntent = new Intent(Settings.
ACTION_INTERNAL_STORAGE_SETTINGS);
if (storageSettingsIntent.resolveActivity(context.getPackageManager()) != null) {
startActivity(storageSettingsIntent);
}
return;
}
return;
}
// check for existing file with the same name

View File

@@ -1,13 +1,11 @@
package org.schabi.newpipe.error;
package org.schabi.newpipe.error
import android.content.Context;
import androidx.annotation.NonNull;
import org.acra.ReportField;
import org.acra.data.CrashReportData;
import org.acra.sender.ReportSender;
import org.schabi.newpipe.R;
import android.content.Context
import org.acra.ReportField
import org.acra.data.CrashReportData
import org.acra.sender.ReportSender
import org.schabi.newpipe.R
import org.schabi.newpipe.error.ErrorUtil.Companion.openActivity
/*
* Created by Christian Schabesberger on 13.09.16.
@@ -28,16 +26,19 @@ import org.schabi.newpipe.R;
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
class AcraReportSender : ReportSender {
override fun send(context: Context, errorContent: CrashReportData) {
public class AcraReportSender implements ReportSender {
@Override
public void send(@NonNull final Context context, @NonNull final CrashReportData report) {
ErrorUtil.openActivity(context, new ErrorInfo(
new String[]{report.getString(ReportField.STACK_TRACE)},
openActivity(
context,
ErrorInfo(
errorContent.getString(ReportField.STACK_TRACE)?.let { arrayOf(it) }
?: emptyArray(),
UserAction.UI_ERROR,
ErrorInfo.SERVICE_NONE,
"ACRA report",
R.string.app_ui_crash));
R.string.app_ui_crash
)
)
}
}

View File

@@ -1,44 +0,0 @@
package org.schabi.newpipe.error;
import android.content.Context;
import androidx.annotation.NonNull;
import com.google.auto.service.AutoService;
import org.acra.config.CoreConfiguration;
import org.acra.sender.ReportSender;
import org.acra.sender.ReportSenderFactory;
import org.schabi.newpipe.App;
/*
* Created by Christian Schabesberger on 13.09.16.
*
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
* AcraReportSenderFactory.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* Used by ACRA in {@link App}.initAcra() as the factory for report senders.
*/
@AutoService(ReportSenderFactory.class)
public class AcraReportSenderFactory implements ReportSenderFactory {
@NonNull
public ReportSender create(@NonNull final Context context,
@NonNull final CoreConfiguration config) {
return new AcraReportSender();
}
}

View File

@@ -1,348 +0,0 @@
package org.schabi.newpipe.error;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.IntentCompat;
import com.grack.nanojson.JsonWriter;
import org.schabi.newpipe.BuildConfig;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.ActivityErrorBinding;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.ThemeHelper;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.stream.Collectors;
/*
* Created by Christian Schabesberger on 24.10.15.
*
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* ErrorActivity.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* <
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* <
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* This activity is used to show error details and allow reporting them in various ways. Use {@link
* ErrorUtil#openActivity(Context, ErrorInfo)} to correctly open this activity.
*/
public class ErrorActivity extends AppCompatActivity {
// LOG TAGS
public static final String TAG = ErrorActivity.class.toString();
// BUNDLE TAGS
public static final String ERROR_INFO = "error_info";
public static final String ERROR_EMAIL_ADDRESS = "crashreport@newpipe.schabi.org";
public static final String ERROR_EMAIL_SUBJECT = "Exception in ";
public static final String ERROR_GITHUB_ISSUE_URL =
"https://github.com/TeamNewPipe/NewPipe/issues";
public static final DateTimeFormatter CURRENT_TIMESTAMP_FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
private ErrorInfo errorInfo;
private String currentTimeStamp;
private ActivityErrorBinding activityErrorBinding;
////////////////////////////////////////////////////////////////////////
// Activity lifecycle
////////////////////////////////////////////////////////////////////////
@Override
protected void onCreate(final Bundle savedInstanceState) {
assureCorrectAppLanguage(this);
super.onCreate(savedInstanceState);
ThemeHelper.setDayNightMode(this);
ThemeHelper.setTheme(this);
activityErrorBinding = ActivityErrorBinding.inflate(getLayoutInflater());
setContentView(activityErrorBinding.getRoot());
final Intent intent = getIntent();
setSupportActionBar(activityErrorBinding.toolbarLayout.toolbar);
final ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.setDisplayHomeAsUpEnabled(true);
actionBar.setTitle(R.string.error_report_title);
actionBar.setDisplayShowTitleEnabled(true);
}
errorInfo = IntentCompat.getParcelableExtra(intent, ERROR_INFO, ErrorInfo.class);
// important add guru meditation
addGuruMeditation();
currentTimeStamp = CURRENT_TIMESTAMP_FORMATTER.format(LocalDateTime.now());
activityErrorBinding.errorReportEmailButton.setOnClickListener(v ->
openPrivacyPolicyDialog(this, "EMAIL"));
activityErrorBinding.errorReportCopyButton.setOnClickListener(v ->
ShareUtils.copyToClipboard(this, buildMarkdown()));
activityErrorBinding.errorReportGitHubButton.setOnClickListener(v ->
openPrivacyPolicyDialog(this, "GITHUB"));
// normal bugreport
buildInfo(errorInfo);
activityErrorBinding.errorMessageView.setText(errorInfo.getMessageStringId());
activityErrorBinding.errorView.setText(formErrorText(errorInfo.getStackTraces()));
// print stack trace once again for debugging:
for (final String e : errorInfo.getStackTraces()) {
Log.e(TAG, e);
}
}
@Override
public boolean onCreateOptionsMenu(final Menu menu) {
final MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.error_menu, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
onBackPressed();
return true;
case R.id.menu_item_share_error:
ShareUtils.shareText(getApplicationContext(),
getString(R.string.error_report_title), buildJson());
return true;
default:
return false;
}
}
private void openPrivacyPolicyDialog(final Context context, final String action) {
new AlertDialog.Builder(context)
.setIcon(android.R.drawable.ic_dialog_alert)
.setTitle(R.string.privacy_policy_title)
.setMessage(R.string.start_accept_privacy_policy)
.setCancelable(false)
.setNeutralButton(R.string.read_privacy_policy, (dialog, which) ->
ShareUtils.openUrlInApp(context,
context.getString(R.string.privacy_policy_url)))
.setPositiveButton(R.string.accept, (dialog, which) -> {
if (action.equals("EMAIL")) { // send on email
final Intent i = new Intent(Intent.ACTION_SENDTO)
.setData(Uri.parse("mailto:")) // only email apps should handle this
.putExtra(Intent.EXTRA_EMAIL, new String[]{ERROR_EMAIL_ADDRESS})
.putExtra(Intent.EXTRA_SUBJECT, ERROR_EMAIL_SUBJECT
+ getString(R.string.app_name) + " "
+ BuildConfig.VERSION_NAME)
.putExtra(Intent.EXTRA_TEXT, buildJson());
ShareUtils.openIntentInApp(context, i);
} else if (action.equals("GITHUB")) { // open the NewPipe issue page on GitHub
ShareUtils.openUrlInApp(this, ERROR_GITHUB_ISSUE_URL);
}
})
.setNegativeButton(R.string.decline, null)
.show();
}
private String formErrorText(final String[] el) {
final String separator = "-------------------------------------";
return Arrays.stream(el)
.collect(Collectors.joining(separator + "\n", separator + "\n", separator));
}
/**
* Get the checked activity.
*
* @param returnActivity the activity to return to
* @return the casted return activity or null
*/
@Nullable
static Class<? extends Activity> getReturnActivity(final Class<?> returnActivity) {
Class<? extends Activity> checkedReturnActivity = null;
if (returnActivity != null) {
if (Activity.class.isAssignableFrom(returnActivity)) {
checkedReturnActivity = returnActivity.asSubclass(Activity.class);
} else {
checkedReturnActivity = MainActivity.class;
}
}
return checkedReturnActivity;
}
private void buildInfo(final ErrorInfo info) {
String text = "";
activityErrorBinding.errorInfoLabelsView.setText(getString(R.string.info_labels)
.replace("\\n", "\n"));
text += getUserActionString(info.getUserAction()) + "\n"
+ info.getRequest() + "\n"
+ getContentLanguageString() + "\n"
+ getContentCountryString() + "\n"
+ getAppLanguage() + "\n"
+ info.getServiceName() + "\n"
+ currentTimeStamp + "\n"
+ getPackageName() + "\n"
+ BuildConfig.VERSION_NAME + "\n"
+ getOsString();
activityErrorBinding.errorInfosView.setText(text);
}
private String buildJson() {
try {
return JsonWriter.string()
.object()
.value("user_action", getUserActionString(errorInfo.getUserAction()))
.value("request", errorInfo.getRequest())
.value("content_language", getContentLanguageString())
.value("content_country", getContentCountryString())
.value("app_language", getAppLanguage())
.value("service", errorInfo.getServiceName())
.value("package", getPackageName())
.value("version", BuildConfig.VERSION_NAME)
.value("os", getOsString())
.value("time", currentTimeStamp)
.array("exceptions", Arrays.asList(errorInfo.getStackTraces()))
.value("user_comment", activityErrorBinding.errorCommentBox.getText()
.toString())
.end()
.done();
} catch (final Throwable e) {
Log.e(TAG, "Error while erroring: Could not build json");
e.printStackTrace();
}
return "";
}
private String buildMarkdown() {
try {
final StringBuilder htmlErrorReport = new StringBuilder();
final String userComment = activityErrorBinding.errorCommentBox.getText().toString();
if (!userComment.isEmpty()) {
htmlErrorReport.append(userComment).append("\n");
}
// basic error info
htmlErrorReport
.append("## Exception")
.append("\n* __User Action:__ ")
.append(getUserActionString(errorInfo.getUserAction()))
.append("\n* __Request:__ ").append(errorInfo.getRequest())
.append("\n* __Content Country:__ ").append(getContentCountryString())
.append("\n* __Content Language:__ ").append(getContentLanguageString())
.append("\n* __App Language:__ ").append(getAppLanguage())
.append("\n* __Service:__ ").append(errorInfo.getServiceName())
.append("\n* __Version:__ ").append(BuildConfig.VERSION_NAME)
.append("\n* __OS:__ ").append(getOsString()).append("\n");
// Collapse all logs to a single paragraph when there are more than one
// to keep the GitHub issue clean.
if (errorInfo.getStackTraces().length > 1) {
htmlErrorReport
.append("<details><summary><b>Exceptions (")
.append(errorInfo.getStackTraces().length)
.append(")</b></summary><p>\n");
}
// add the logs
for (int i = 0; i < errorInfo.getStackTraces().length; i++) {
htmlErrorReport.append("<details><summary><b>Crash log ");
if (errorInfo.getStackTraces().length > 1) {
htmlErrorReport.append(i + 1);
}
htmlErrorReport.append("</b>")
.append("</summary><p>\n")
.append("\n```\n").append(errorInfo.getStackTraces()[i]).append("\n```\n")
.append("</details>\n");
}
// make sure to close everything
if (errorInfo.getStackTraces().length > 1) {
htmlErrorReport.append("</p></details>\n");
}
htmlErrorReport.append("<hr>\n");
return htmlErrorReport.toString();
} catch (final Throwable e) {
Log.e(TAG, "Error while erroring: Could not build markdown");
e.printStackTrace();
return "";
}
}
private String getUserActionString(final UserAction userAction) {
if (userAction == null) {
return "Your description is in another castle.";
} else {
return userAction.getMessage();
}
}
private String getContentCountryString() {
return Localization.getPreferredContentCountry(this).getCountryCode();
}
private String getContentLanguageString() {
return Localization.getPreferredLocalization(this).getLocalizationCode();
}
private String getAppLanguage() {
return Localization.getAppLocale(getApplicationContext()).toString();
}
private String getOsString() {
final String osBase = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
? Build.VERSION.BASE_OS : "Android";
return System.getProperty("os.name")
+ " " + (osBase.isEmpty() ? "Android" : osBase)
+ " " + Build.VERSION.RELEASE
+ " - " + Build.VERSION.SDK_INT;
}
private void addGuruMeditation() {
//just an easter egg
String text = activityErrorBinding.errorSorryView.getText().toString();
text += "\n" + getString(R.string.guru_meditation);
activityErrorBinding.errorSorryView.setText(text);
}
}

View File

@@ -0,0 +1,353 @@
package org.schabi.newpipe.error
import android.app.Activity
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.IntentCompat
import com.grack.nanojson.JsonWriter
import org.schabi.newpipe.BuildConfig
import org.schabi.newpipe.MainActivity
import org.schabi.newpipe.R
import org.schabi.newpipe.databinding.ActivityErrorBinding
import org.schabi.newpipe.util.Localization
import org.schabi.newpipe.util.ThemeHelper
import org.schabi.newpipe.util.external_communication.ShareUtils
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.util.Arrays
import java.util.stream.Collectors
/*
* Created by Christian Schabesberger on 24.10.15.
*
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* ErrorActivity.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* <
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* <
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* This activity is used to show error details and allow reporting them in various ways. Use [ ][ErrorUtil.openActivity] to correctly open this activity.
*/
class ErrorActivity : AppCompatActivity() {
private lateinit var errorInfo: ErrorInfo
private lateinit var currentTimeStamp: String
private lateinit var activityErrorBinding: ActivityErrorBinding
// //////////////////////////////////////////////////////////////////////
// Activity lifecycle
// //////////////////////////////////////////////////////////////////////
override fun onCreate(savedInstanceState: Bundle?) {
Localization.assureCorrectAppLanguage(this)
super.onCreate(savedInstanceState)
ThemeHelper.setDayNightMode(this)
ThemeHelper.setTheme(this)
activityErrorBinding = ActivityErrorBinding.inflate(
layoutInflater
)
setContentView(activityErrorBinding.root)
val intent = intent
setSupportActionBar(activityErrorBinding.toolbarLayout.toolbar)
val actionBar = supportActionBar
if (actionBar != null) {
actionBar.setDisplayHomeAsUpEnabled(true)
actionBar.setTitle(R.string.error_report_title)
actionBar.setDisplayShowTitleEnabled(true)
}
errorInfo = IntentCompat.getParcelableExtra(intent, ERROR_INFO, ErrorInfo::class.java)!!
// important add guru meditation
addGuruMeditation()
currentTimeStamp = CURRENT_TIMESTAMP_FORMATTER.format(LocalDateTime.now())
activityErrorBinding.errorReportEmailButton.setOnClickListener { _: View? ->
openPrivacyPolicyDialog(
this,
"EMAIL"
)
}
activityErrorBinding.errorReportCopyButton.setOnClickListener { _: View? ->
ShareUtils.copyToClipboard(
this,
buildMarkdown()
)
}
activityErrorBinding.errorReportGitHubButton.setOnClickListener { _: View? ->
openPrivacyPolicyDialog(
this,
"GITHUB"
)
}
// normal bugreport
buildInfo(errorInfo)
activityErrorBinding.errorMessageView.setText(errorInfo.messageStringId)
activityErrorBinding.errorView.text = formErrorText(errorInfo.stackTraces)
// print stack trace once again for debugging:
for (e in errorInfo.stackTraces) {
Log.e(TAG, e)
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
val inflater = menuInflater
inflater.inflate(R.menu.error_menu, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
android.R.id.home -> {
onBackPressed()
true
}
R.id.menu_item_share_error -> {
ShareUtils.shareText(
applicationContext,
getString(R.string.error_report_title), buildJson()
)
true
}
else -> false
}
}
private fun openPrivacyPolicyDialog(context: Context, action: String) {
AlertDialog.Builder(context)
.setIcon(android.R.drawable.ic_dialog_alert)
.setTitle(R.string.privacy_policy_title)
.setMessage(R.string.start_accept_privacy_policy)
.setCancelable(false)
.setNeutralButton(R.string.read_privacy_policy) { _: DialogInterface?, _: Int ->
ShareUtils.openUrlInApp(
context,
context.getString(R.string.privacy_policy_url)
)
}
.setPositiveButton(R.string.accept) { _: DialogInterface?, _: Int ->
if (action == "EMAIL") { // send on email
val i = Intent(Intent.ACTION_SENDTO)
.setData(Uri.parse("mailto:")) // only email apps should handle this
.putExtra(Intent.EXTRA_EMAIL, arrayOf(ERROR_EMAIL_ADDRESS))
.putExtra(
Intent.EXTRA_SUBJECT,
ERROR_EMAIL_SUBJECT +
getString(R.string.app_name) + " " +
BuildConfig.VERSION_NAME
)
.putExtra(Intent.EXTRA_TEXT, buildJson())
ShareUtils.openIntentInApp(context, i)
} else if (action == "GITHUB") { // open the NewPipe issue page on GitHub
ShareUtils.openUrlInApp(this, ERROR_GITHUB_ISSUE_URL)
}
}
.setNegativeButton(R.string.decline, null)
.show()
}
private fun formErrorText(el: Array<String>): String {
val separator = "-------------------------------------"
return Arrays.stream(el)
.collect(
Collectors.joining(
"""
$separator
""".trimIndent(),
"""
$separator
""".trimIndent(),
separator
)
)
}
private fun buildInfo(info: ErrorInfo) {
var text = ""
activityErrorBinding.errorInfoLabelsView.text = getString(R.string.info_labels)
.replace("\\n", "\n")
text += """
${getUserActionString(info.userAction)}
${info.request}
$contentLanguageString
$contentCountryString
$appLanguage
${info.serviceName}
$currentTimeStamp
$packageName
${BuildConfig.VERSION_NAME}
$osString
""".trimIndent()
activityErrorBinding.errorInfosView.text = text
}
private fun buildJson(): String {
try {
return JsonWriter.string()
.`object`()
.value("user_action", getUserActionString(errorInfo.userAction))
.value("request", errorInfo.request)
.value("content_language", contentLanguageString)
.value("content_country", contentCountryString)
.value("app_language", appLanguage)
.value("service", errorInfo.serviceName)
.value("package", packageName)
.value("version", BuildConfig.VERSION_NAME)
.value("os", osString)
.value("time", currentTimeStamp)
.array("exceptions", listOf(*errorInfo.stackTraces))
.value(
"user_comment",
activityErrorBinding.errorCommentBox.text
.toString()
)
.end()
.done()
} catch (e: Throwable) {
Log.e(TAG, "Error while erroring: Could not build json")
e.printStackTrace()
}
return ""
}
private fun buildMarkdown(): String {
return try {
val htmlErrorReport = StringBuilder()
val userComment = activityErrorBinding.errorCommentBox.text.toString()
if (userComment.isNotEmpty()) {
htmlErrorReport.append(userComment).append("\n")
}
// basic error info
htmlErrorReport
.append("## Exception")
.append("\n* __User Action:__ ")
.append(getUserActionString(errorInfo.userAction))
.append("\n* __Request:__ ").append(errorInfo.request)
.append("\n* __Content Country:__ ").append(contentCountryString)
.append("\n* __Content Language:__ ").append(contentLanguageString)
.append("\n* __App Language:__ ").append(appLanguage)
.append("\n* __Service:__ ").append(errorInfo.serviceName)
.append("\n* __Version:__ ").append(BuildConfig.VERSION_NAME)
.append("\n* __OS:__ ").append(osString).append("\n")
// Collapse all logs to a single paragraph when there are more than one
// to keep the GitHub issue clean.
if (errorInfo.stackTraces.size > 1) {
htmlErrorReport
.append("<details><summary><b>Exceptions (")
.append(errorInfo.stackTraces.size)
.append(")</b></summary><p>\n")
}
// add the logs
for (i in errorInfo.stackTraces.indices) {
htmlErrorReport.append("<details><summary><b>Crash log ")
if (errorInfo.stackTraces.size > 1) {
htmlErrorReport.append(i + 1)
}
htmlErrorReport.append("</b>")
.append("</summary><p>\n")
.append("\n```\n").append(errorInfo.stackTraces[i]).append("\n```\n")
.append("</details>\n")
}
// make sure to close everything
if (errorInfo.stackTraces.size > 1) {
htmlErrorReport.append("</p></details>\n")
}
htmlErrorReport.append("<hr>\n")
htmlErrorReport.toString()
} catch (e: Throwable) {
Log.e(TAG, "Error while erroring: Could not build markdown")
e.printStackTrace()
""
}
}
private fun getUserActionString(userAction: UserAction?): String {
return userAction?.message ?: "Your description is in another castle."
}
private val contentCountryString: String
get() = Localization.getPreferredContentCountry(this).countryCode
private val contentLanguageString: String
get() = Localization.getPreferredLocalization(this).localizationCode
private val appLanguage: String
get() = Localization.getAppLocale(applicationContext).toString()
private val osString: String
get() {
val osBase =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) Build.VERSION.BASE_OS else "Android"
return (
(System.getProperty("os.name") ?: "unknown operating system (os.name not set)") +
" " + (osBase.ifEmpty { "Android" }) +
" " + Build.VERSION.RELEASE +
" - " + Build.VERSION.SDK_INT
)
}
private fun addGuruMeditation() {
// just an easter egg
var text = activityErrorBinding.errorSorryView.text.toString()
text += """
${getString(R.string.guru_meditation)}
""".trimIndent()
activityErrorBinding.errorSorryView.text = text
}
companion object {
// LOG TAGS
val TAG = ErrorActivity::class.java.toString()
// BUNDLE TAGS
const val ERROR_INFO = "error_info"
const val ERROR_EMAIL_ADDRESS = "crashreport@newpipe.schabi.org"
const val ERROR_EMAIL_SUBJECT = "Exception in "
const val ERROR_GITHUB_ISSUE_URL = "https://github.com/TeamNewPipe/NewPipe/issues"
val CURRENT_TIMESTAMP_FORMATTER: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")
/**
* Get the checked activity.
*
* @param returnActivity the activity to return to
* @return the casted return activity or null
*/
@JvmStatic
fun getReturnActivity(returnActivity: Class<*>?): Class<out Activity?>? {
var checkedReturnActivity: Class<out Activity?>? = null
if (returnActivity != null) {
checkedReturnActivity = if (Activity::class.java.isAssignableFrom(returnActivity)) {
returnActivity.asSubclass(Activity::class.java)
} else {
MainActivity::class.java
}
}
return checkedReturnActivity
}
}
}

View File

@@ -150,7 +150,7 @@ class ErrorPanelHelper(
errorActionButton.setOnClickListener(listener)
}
fun showAndSetOpenInBrowserButtonAction(
private fun showAndSetOpenInBrowserButtonAction(
errorInfo: ErrorInfo
) {
errorOpenInBrowserButton.isVisible = true

View File

@@ -1,238 +0,0 @@
package org.schabi.newpipe.error;
import android.annotation.SuppressLint;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.webkit.CookieManager;
import android.webkit.WebResourceRequest;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.NavUtils;
import androidx.preference.PreferenceManager;
import org.schabi.newpipe.DownloaderImpl;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.ActivityRecaptchaBinding;
import org.schabi.newpipe.extractor.utils.Utils;
import org.schabi.newpipe.util.ThemeHelper;
import java.io.UnsupportedEncodingException;
/*
* Created by beneth <bmauduit@beneth.fr> on 06.12.16.
*
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
* ReCaptchaActivity.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public class ReCaptchaActivity extends AppCompatActivity {
public static final int RECAPTCHA_REQUEST = 10;
public static final String RECAPTCHA_URL_EXTRA = "recaptcha_url_extra";
public static final String TAG = ReCaptchaActivity.class.toString();
public static final String YT_URL = "https://www.youtube.com";
public static final String RECAPTCHA_COOKIES_KEY = "recaptcha_cookies";
public static String sanitizeRecaptchaUrl(@Nullable final String url) {
if (url == null || url.trim().isEmpty()) {
return YT_URL; // YouTube is the most likely service to have thrown a recaptcha
} else {
// remove "pbj=1" parameter from YouYube urls, as it makes the page JSON and not HTML
return url.replace("&pbj=1", "").replace("pbj=1&", "").replace("?pbj=1", "");
}
}
private ActivityRecaptchaBinding recaptchaBinding;
private String foundCookies = "";
@SuppressLint("SetJavaScriptEnabled")
@Override
protected void onCreate(final Bundle savedInstanceState) {
ThemeHelper.setTheme(this);
super.onCreate(savedInstanceState);
recaptchaBinding = ActivityRecaptchaBinding.inflate(getLayoutInflater());
setContentView(recaptchaBinding.getRoot());
setSupportActionBar(recaptchaBinding.toolbar);
final String url = sanitizeRecaptchaUrl(getIntent().getStringExtra(RECAPTCHA_URL_EXTRA));
// set return to Cancel by default
setResult(RESULT_CANCELED);
// enable Javascript
final WebSettings webSettings = recaptchaBinding.reCaptchaWebView.getSettings();
webSettings.setJavaScriptEnabled(true);
webSettings.setUserAgentString(DownloaderImpl.USER_AGENT);
recaptchaBinding.reCaptchaWebView.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(final WebView view,
final WebResourceRequest request) {
if (MainActivity.DEBUG) {
Log.d(TAG, "shouldOverrideUrlLoading: url=" + request.getUrl().toString());
}
handleCookiesFromUrl(request.getUrl().toString());
return false;
}
@Override
public void onPageFinished(final WebView view, final String url) {
super.onPageFinished(view, url);
handleCookiesFromUrl(url);
}
});
// cleaning cache, history and cookies from webView
recaptchaBinding.reCaptchaWebView.clearCache(true);
recaptchaBinding.reCaptchaWebView.clearHistory();
CookieManager.getInstance().removeAllCookies(null);
recaptchaBinding.reCaptchaWebView.loadUrl(url);
}
@Override
public boolean onCreateOptionsMenu(final Menu menu) {
getMenuInflater().inflate(R.menu.menu_recaptcha, menu);
final ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.setDisplayHomeAsUpEnabled(false);
actionBar.setTitle(R.string.title_activity_recaptcha);
actionBar.setSubtitle(R.string.subtitle_activity_recaptcha);
}
return true;
}
@Override
public void onBackPressed() {
saveCookiesAndFinish();
}
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
if (item.getItemId() == R.id.menu_item_done) {
saveCookiesAndFinish();
return true;
}
return false;
}
private void saveCookiesAndFinish() {
// try to get cookies of unclosed page
handleCookiesFromUrl(recaptchaBinding.reCaptchaWebView.getUrl());
if (MainActivity.DEBUG) {
Log.d(TAG, "saveCookiesAndFinish: foundCookies=" + foundCookies);
}
if (!foundCookies.isEmpty()) {
// save cookies to preferences
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(
getApplicationContext());
final String key = getApplicationContext().getString(R.string.recaptcha_cookies_key);
prefs.edit().putString(key, foundCookies).apply();
// give cookies to Downloader class
DownloaderImpl.getInstance().setCookie(RECAPTCHA_COOKIES_KEY, foundCookies);
setResult(RESULT_OK);
}
// Navigate to blank page (unloads youtube to prevent background playback)
recaptchaBinding.reCaptchaWebView.loadUrl("about:blank");
final Intent intent = new Intent(this, org.schabi.newpipe.MainActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
NavUtils.navigateUpTo(this, intent);
}
private void handleCookiesFromUrl(@Nullable final String url) {
if (MainActivity.DEBUG) {
Log.d(TAG, "handleCookiesFromUrl: url=" + (url == null ? "null" : url));
}
if (url == null) {
return;
}
final String cookies = CookieManager.getInstance().getCookie(url);
handleCookies(cookies);
// sometimes cookies are inside the url
final int abuseStart = url.indexOf("google_abuse=");
if (abuseStart != -1) {
final int abuseEnd = url.indexOf("+path");
try {
String abuseCookie = url.substring(abuseStart + 13, abuseEnd);
abuseCookie = Utils.decodeUrlUtf8(abuseCookie);
handleCookies(abuseCookie);
} catch (UnsupportedEncodingException | StringIndexOutOfBoundsException e) {
if (MainActivity.DEBUG) {
e.printStackTrace();
Log.d(TAG, "handleCookiesFromUrl: invalid google abuse starting at "
+ abuseStart + " and ending at " + abuseEnd + " for url " + url);
}
}
}
}
private void handleCookies(@Nullable final String cookies) {
if (MainActivity.DEBUG) {
Log.d(TAG, "handleCookies: cookies=" + (cookies == null ? "null" : cookies));
}
if (cookies == null) {
return;
}
addYoutubeCookies(cookies);
// add here methods to extract cookies for other services
}
private void addYoutubeCookies(@NonNull final String cookies) {
if (cookies.contains("s_gl=") || cookies.contains("goojf=")
|| cookies.contains("VISITOR_INFO1_LIVE=")
|| cookies.contains("GOOGLE_ABUSE_EXEMPTION=")) {
// youtube seems to also need the other cookies:
addCookie(cookies);
}
}
private void addCookie(final String cookie) {
if (foundCookies.contains(cookie)) {
return;
}
if (foundCookies.isEmpty() || foundCookies.endsWith("; ")) {
foundCookies += cookie;
} else if (foundCookies.endsWith(";")) {
foundCookies += " " + cookie;
} else {
foundCookies += "; " + cookie;
}
}
}

View File

@@ -0,0 +1,227 @@
package org.schabi.newpipe.error
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.webkit.CookieManager
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.NavUtils
import androidx.preference.PreferenceManager
import org.schabi.newpipe.DownloaderImpl
import org.schabi.newpipe.MainActivity
import org.schabi.newpipe.R
import org.schabi.newpipe.databinding.ActivityRecaptchaBinding
import org.schabi.newpipe.extractor.utils.Utils
import org.schabi.newpipe.util.ThemeHelper
import java.io.UnsupportedEncodingException
/*
* Created by beneth <bmauduit@beneth.fr> on 06.12.16.
*
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
* ReCaptchaActivity.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
class ReCaptchaActivity : AppCompatActivity() {
private lateinit var recaptchaBinding: ActivityRecaptchaBinding
private var foundCookies = ""
@SuppressLint("SetJavaScriptEnabled")
override fun onCreate(savedInstanceState: Bundle?) {
ThemeHelper.setTheme(this)
super.onCreate(savedInstanceState)
recaptchaBinding = ActivityRecaptchaBinding.inflate(
layoutInflater
)
setContentView(recaptchaBinding.root)
setSupportActionBar(recaptchaBinding.toolbar)
val url = sanitizeRecaptchaUrl(intent.getStringExtra(RECAPTCHA_URL_EXTRA))
// set return to Cancel by default
setResult(RESULT_CANCELED)
// enable Javascript
val webSettings = recaptchaBinding.reCaptchaWebView.settings
webSettings.javaScriptEnabled = true
webSettings.userAgentString = DownloaderImpl.USER_AGENT
recaptchaBinding.reCaptchaWebView.webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(
view: WebView,
request: WebResourceRequest
): Boolean {
if (MainActivity.DEBUG) {
Log.d(TAG, "shouldOverrideUrlLoading: url=" + request.url.toString())
}
handleCookiesFromUrl(request.url.toString())
return false
}
override fun onPageFinished(view: WebView, url: String) {
super.onPageFinished(view, url)
handleCookiesFromUrl(url)
}
}
// cleaning cache, history and cookies from webView
recaptchaBinding.reCaptchaWebView.clearCache(true)
recaptchaBinding.reCaptchaWebView.clearHistory()
CookieManager.getInstance().removeAllCookies(null)
recaptchaBinding.reCaptchaWebView.loadUrl(url)
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_recaptcha, menu)
val actionBar = supportActionBar
if (actionBar != null) {
actionBar.setDisplayHomeAsUpEnabled(false)
actionBar.setTitle(R.string.title_activity_recaptcha)
actionBar.setSubtitle(R.string.subtitle_activity_recaptcha)
}
return true
}
@Deprecated("Deprecated in Java")
override fun onBackPressed() {
saveCookiesAndFinish()
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == R.id.menu_item_done) {
saveCookiesAndFinish()
return true
}
return false
}
private fun saveCookiesAndFinish() {
// try to get cookies of unclosed page
handleCookiesFromUrl(recaptchaBinding.reCaptchaWebView.url)
if (MainActivity.DEBUG) {
Log.d(TAG, "saveCookiesAndFinish: foundCookies=$foundCookies")
}
if (foundCookies.isNotEmpty()) {
// save cookies to preferences
val prefs = PreferenceManager.getDefaultSharedPreferences(
applicationContext
)
val key = applicationContext.getString(R.string.recaptcha_cookies_key)
prefs.edit().putString(key, foundCookies).apply()
// give cookies to Downloader class
DownloaderImpl.getInstance().setCookie(RECAPTCHA_COOKIES_KEY, foundCookies)
setResult(RESULT_OK)
}
// Navigate to blank page (unloads youtube to prevent background playback)
recaptchaBinding.reCaptchaWebView.loadUrl("about:blank")
val intent = Intent(this, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
NavUtils.navigateUpTo(this, intent)
}
private fun handleCookiesFromUrl(url: String?) {
if (MainActivity.DEBUG) {
Log.d(TAG, "handleCookiesFromUrl: url=" + (url ?: "null"))
}
if (url == null) {
return
}
val cookies = CookieManager.getInstance().getCookie(url)
handleCookies(cookies)
// sometimes cookies are inside the url
val abuseStart = url.indexOf("google_abuse=")
if (abuseStart != -1) {
val abuseEnd = url.indexOf("+path")
try {
var abuseCookie: String? = url.substring(abuseStart + 13, abuseEnd)
abuseCookie = Utils.decodeUrlUtf8(abuseCookie)
handleCookies(abuseCookie)
} catch (e: UnsupportedEncodingException) {
if (MainActivity.DEBUG) {
e.printStackTrace()
Log.d(
TAG,
"handleCookiesFromUrl: invalid google abuse starting at " +
abuseStart + " and ending at " + abuseEnd + " for url " + url
)
}
} catch (e: StringIndexOutOfBoundsException) {
if (MainActivity.DEBUG) {
e.printStackTrace()
Log.d(
TAG,
"handleCookiesFromUrl: invalid google abuse starting at " +
abuseStart + " and ending at " + abuseEnd + " for url " + url
)
}
}
}
}
private fun handleCookies(cookies: String?) {
if (MainActivity.DEBUG) {
Log.d(TAG, "handleCookies: cookies=" + (cookies ?: "null"))
}
if (cookies == null) {
return
}
addYoutubeCookies(cookies)
// add here methods to extract cookies for other services
}
private fun addYoutubeCookies(cookies: String) {
if (cookies.contains("s_gl=") || cookies.contains("goojf=") ||
cookies.contains("VISITOR_INFO1_LIVE=") ||
cookies.contains("GOOGLE_ABUSE_EXEMPTION=")
) {
// youtube seems to also need the other cookies:
addCookie(cookies)
}
}
private fun addCookie(cookie: String) {
if (foundCookies.contains(cookie)) {
return
}
foundCookies += if (foundCookies.isEmpty() || foundCookies.endsWith("; ")) {
cookie
} else if (foundCookies.endsWith(";")) {
" $cookie"
} else {
"; $cookie"
}
}
companion object {
const val RECAPTCHA_REQUEST = 10
const val RECAPTCHA_URL_EXTRA = "recaptcha_url_extra"
val TAG = ReCaptchaActivity::class.java.toString()
const val YT_URL = "https://www.youtube.com"
const val RECAPTCHA_COOKIES_KEY = "recaptcha_cookies"
fun sanitizeRecaptchaUrl(url: String?): String {
return if (url == null || url.trim { it <= ' ' }.isEmpty()) {
YT_URL // YouTube is the most likely service to have thrown a recaptcha
} else {
// remove "pbj=1" parameter from YouYube urls, as it makes the page JSON and not HTML
url.replace("&pbj=1", "").replace("pbj=1&", "").replace("?pbj=1", "")
}
}
}
}

View File

@@ -1,46 +0,0 @@
package org.schabi.newpipe.error;
/**
* The user actions that can cause an error.
*/
public enum UserAction {
USER_REPORT("user report"),
UI_ERROR("ui error"),
DATABASE_IMPORT_EXPORT("database import or export"),
SUBSCRIPTION_CHANGE("subscription change"),
SUBSCRIPTION_UPDATE("subscription update"),
SUBSCRIPTION_GET("get subscription"),
SUBSCRIPTION_IMPORT_EXPORT("subscription import or export"),
LOAD_IMAGE("load image"),
SOMETHING_ELSE("something else"),
SEARCHED("searched"),
GET_SUGGESTIONS("get suggestions"),
REQUESTED_STREAM("requested stream"),
REQUESTED_CHANNEL("requested channel"),
REQUESTED_PLAYLIST("requested playlist"),
REQUESTED_KIOSK("requested kiosk"),
REQUESTED_COMMENTS("requested comments"),
REQUESTED_COMMENT_REPLIES("requested comment replies"),
REQUESTED_FEED("requested feed"),
REQUESTED_BOOKMARK("bookmark"),
DELETE_FROM_HISTORY("delete from history"),
PLAY_STREAM("play stream"),
DOWNLOAD_OPEN_DIALOG("download open dialog"),
DOWNLOAD_POSTPROCESSING("download post-processing"),
DOWNLOAD_FAILED("download failed"),
NEW_STREAMS_NOTIFICATIONS("new streams notifications"),
PREFERENCES_MIGRATION("migration of preferences"),
SHARE_TO_NEWPIPE("share to newpipe"),
CHECK_FOR_NEW_APP_VERSION("check for new app version"),
OPEN_INFO_ITEM_DIALOG("open info item dialog");
private final String message;
UserAction(final String message) {
this.message = message;
}
public String getMessage() {
return message;
}
}

View File

@@ -0,0 +1,32 @@
package org.schabi.newpipe.error
/**
* The user actions that can cause an error.
*/
enum class UserAction(val message: String) {
USER_REPORT("user report"), UI_ERROR("ui error"), SUBSCRIPTION_CHANGE("subscription change"), SUBSCRIPTION_UPDATE(
"subscription update"
),
SUBSCRIPTION_GET("get subscription"), SUBSCRIPTION_IMPORT_EXPORT("subscription import or export"), LOAD_IMAGE(
"load image"
),
SOMETHING_ELSE("something else"), SEARCHED("searched"), GET_SUGGESTIONS("get suggestions"), REQUESTED_STREAM(
"requested stream"
),
REQUESTED_CHANNEL("requested channel"), REQUESTED_PLAYLIST("requested playlist"), REQUESTED_KIOSK(
"requested kiosk"
),
REQUESTED_COMMENTS("requested comments"), REQUESTED_COMMENT_REPLIES("requested comment replies"), REQUESTED_FEED(
"requested feed"
),
REQUESTED_BOOKMARK("bookmark"), DELETE_FROM_HISTORY("delete from history"), PLAY_STREAM("play stream"), DOWNLOAD_OPEN_DIALOG(
"download open dialog"
),
DOWNLOAD_POSTPROCESSING("download post-processing"), DOWNLOAD_FAILED("download failed"), NEW_STREAMS_NOTIFICATIONS(
"new streams notifications"
),
PREFERENCES_MIGRATION("migration of preferences"), SHARE_TO_NEWPIPE("share to newpipe"), CHECK_FOR_NEW_APP_VERSION(
"check for new app version"
),
OPEN_INFO_ITEM_DIALOG("open info item dialog")
}

View File

@@ -220,7 +220,7 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
public void commitPlaylistTabs() {
pagerAdapter.getLocalPlaylistFragments()
.stream()
.forEach(LocalPlaylistFragment::saveImmediate);
.forEach(LocalPlaylistFragment::commitChanges);
}
private void updateTabLayoutPosition() {
@@ -282,7 +282,7 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
* Keep reference to LocalPlaylistFragments, because their data can be modified by the user
* during runtime and changes are not committed immediately. However, in some cases,
* the changes need to be committed immediately by calling
* {@link LocalPlaylistFragment#saveImmediate()}.
* {@link LocalPlaylistFragment#commitChanges()}.
* The fragments are removed when {@link LocalPlaylistFragment#onDestroy()} is called.
*/
private final List<LocalPlaylistFragment> localPlaylistFragments = new ArrayList<>();

View File

@@ -30,10 +30,6 @@ public class DescriptionFragment extends BaseDescriptionFragment {
this.streamInfo = streamInfo;
}
public DescriptionFragment() {
// keep empty constructor for IcePick when resuming fragment from memory
}
@Nullable
@Override

View File

@@ -482,7 +482,7 @@ public final class VideoDetailFragment
// commit previous pending changes to database
if (fragment instanceof LocalPlaylistFragment) {
((LocalPlaylistFragment) fragment).saveImmediate();
((LocalPlaylistFragment) fragment).commitChanges();
} else if (fragment instanceof MainFragment) {
((MainFragment) fragment).commitPlaylistTabs();
}

View File

@@ -30,9 +30,6 @@ public class ChannelAboutFragment extends BaseDescriptionFragment {
this.channelInfo = channelInfo;
}
public ChannelAboutFragment() {
// keep empty constructor for IcePick when resuming fragment from memory
}
@Override
protected void initViews(final View rootView, final Bundle savedInstanceState) {

View File

@@ -30,7 +30,6 @@ import org.schabi.newpipe.util.text.TextLinkifier;
import java.util.Queue;
import java.util.function.Supplier;
import icepick.State;
import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
@@ -39,8 +38,7 @@ public final class CommentRepliesFragment
public static final String TAG = CommentRepliesFragment.class.getSimpleName();
@State
CommentsInfoItem commentsInfoItem; // the comment to show replies of
private CommentsInfoItem commentsInfoItem; // the comment to show replies of
private final CompositeDisposable disposables = new CompositeDisposable();

View File

@@ -506,7 +506,7 @@ public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, Playl
Localization.concatenateStrings(
Localization.localizeStreamCount(activity, streamCount),
Localization.getDurationString(playlistOverallDurationSeconds,
isDurationComplete, true))
isDurationComplete))
);
}
}

View File

@@ -14,7 +14,6 @@ import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
import org.schabi.newpipe.info_list.ItemViewMode;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.local.holder.LocalBookmarkPlaylistItemHolder;
import org.schabi.newpipe.local.holder.LocalItemHolder;
import org.schabi.newpipe.local.holder.LocalPlaylistCardItemHolder;
import org.schabi.newpipe.local.holder.LocalPlaylistGridItemHolder;
@@ -25,7 +24,6 @@ import org.schabi.newpipe.local.holder.LocalPlaylistStreamItemHolder;
import org.schabi.newpipe.local.holder.LocalStatisticStreamCardItemHolder;
import org.schabi.newpipe.local.holder.LocalStatisticStreamGridItemHolder;
import org.schabi.newpipe.local.holder.LocalStatisticStreamItemHolder;
import org.schabi.newpipe.local.holder.RemoteBookmarkPlaylistItemHolder;
import org.schabi.newpipe.local.holder.RemotePlaylistCardItemHolder;
import org.schabi.newpipe.local.holder.RemotePlaylistGridItemHolder;
import org.schabi.newpipe.local.holder.RemotePlaylistItemHolder;
@@ -75,12 +73,10 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
private static final int LOCAL_PLAYLIST_HOLDER_TYPE = 0x2000;
private static final int LOCAL_PLAYLIST_GRID_HOLDER_TYPE = 0x2001;
private static final int LOCAL_PLAYLIST_CARD_HOLDER_TYPE = 0x2002;
private static final int LOCAL_BOOKMARK_PLAYLIST_HOLDER_TYPE = 0x2003;
private static final int REMOTE_PLAYLIST_HOLDER_TYPE = 0x3000;
private static final int REMOTE_PLAYLIST_GRID_HOLDER_TYPE = 0x3001;
private static final int REMOTE_PLAYLIST_CARD_HOLDER_TYPE = 0x3002;
private static final int REMOTE_BOOKMARK_PLAYLIST_HOLDER_TYPE = 0x3003;
private final LocalItemBuilder localItemBuilder;
private final ArrayList<LocalItem> localItems;
@@ -91,7 +87,6 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
private View header = null;
private View footer = null;
private ItemViewMode itemViewMode = ItemViewMode.LIST;
private boolean useItemHandle = false;
public LocalItemListAdapter(final Context context) {
recordManager = new HistoryRecordManager(context);
@@ -185,10 +180,6 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
this.itemViewMode = itemViewMode;
}
public void setUseItemHandle(final boolean useItemHandle) {
this.useItemHandle = useItemHandle;
}
public void setHeader(final View header) {
final boolean changed = header != this.header;
this.header = header;
@@ -266,9 +257,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
final LocalItem item = localItems.get(position);
switch (item.getLocalItemType()) {
case PLAYLIST_LOCAL_ITEM:
if (useItemHandle) {
return LOCAL_BOOKMARK_PLAYLIST_HOLDER_TYPE;
} else if (itemViewMode == ItemViewMode.CARD) {
if (itemViewMode == ItemViewMode.CARD) {
return LOCAL_PLAYLIST_CARD_HOLDER_TYPE;
} else if (itemViewMode == ItemViewMode.GRID) {
return LOCAL_PLAYLIST_GRID_HOLDER_TYPE;
@@ -276,9 +265,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
return LOCAL_PLAYLIST_HOLDER_TYPE;
}
case PLAYLIST_REMOTE_ITEM:
if (useItemHandle) {
return REMOTE_BOOKMARK_PLAYLIST_HOLDER_TYPE;
} else if (itemViewMode == ItemViewMode.CARD) {
if (itemViewMode == ItemViewMode.CARD) {
return REMOTE_PLAYLIST_CARD_HOLDER_TYPE;
} else if (itemViewMode == ItemViewMode.GRID) {
return REMOTE_PLAYLIST_GRID_HOLDER_TYPE;
@@ -327,16 +314,12 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
return new LocalPlaylistGridItemHolder(localItemBuilder, parent);
case LOCAL_PLAYLIST_CARD_HOLDER_TYPE:
return new LocalPlaylistCardItemHolder(localItemBuilder, parent);
case LOCAL_BOOKMARK_PLAYLIST_HOLDER_TYPE:
return new LocalBookmarkPlaylistItemHolder(localItemBuilder, parent);
case REMOTE_PLAYLIST_HOLDER_TYPE:
return new RemotePlaylistItemHolder(localItemBuilder, parent);
case REMOTE_PLAYLIST_GRID_HOLDER_TYPE:
return new RemotePlaylistGridItemHolder(localItemBuilder, parent);
case REMOTE_PLAYLIST_CARD_HOLDER_TYPE:
return new RemotePlaylistCardItemHolder(localItemBuilder, parent);
case REMOTE_BOOKMARK_PLAYLIST_HOLDER_TYPE:
return new RemoteBookmarkPlaylistItemHolder(localItemBuilder, parent);
case STREAM_PLAYLIST_HOLDER_TYPE:
return new LocalPlaylistStreamItemHolder(localItemBuilder, parent);
case STREAM_PLAYLIST_GRID_HOLDER_TYPE:

View File

@@ -1,13 +1,10 @@
package org.schabi.newpipe.local.bookmark;
import static org.schabi.newpipe.local.bookmark.MergedPlaylistManager.getMergedOrderedPlaylists;
import android.content.DialogInterface;
import android.os.Bundle;
import android.os.Parcelable;
import android.text.InputType;
import android.util.Log;
import android.util.Pair;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -16,8 +13,6 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.FragmentManager;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
@@ -32,45 +27,29 @@ import org.schabi.newpipe.databinding.DialogEditTextBinding;
import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.local.BaseLocalListFragment;
import org.schabi.newpipe.local.holder.LocalBookmarkPlaylistItemHolder;
import org.schabi.newpipe.local.holder.RemoteBookmarkPlaylistItemHolder;
import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
import org.schabi.newpipe.util.debounce.DebounceSavable;
import org.schabi.newpipe.util.debounce.DebounceSaver;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.OnClickGesture;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import icepick.State;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Flowable;
import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.disposables.Disposable;
public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistLocalItem>, Void>
implements DebounceSavable {
private static final int MINIMUM_INITIAL_DRAG_VELOCITY = 12;
public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistLocalItem>, Void> {
@State
Parcelable itemsListState;
protected Parcelable itemsListState;
private Subscription databaseSubscription;
private CompositeDisposable disposables = new CompositeDisposable();
private LocalPlaylistManager localPlaylistManager;
private RemotePlaylistManager remotePlaylistManager;
private ItemTouchHelper itemTouchHelper;
/* Have the bookmarked playlists been fully loaded from db */
private AtomicBoolean isLoadingComplete;
/* Gives enough time to avoid interrupting user sorting operations */
@Nullable
private DebounceSaver debounceSaver;
private List<Pair<Long, LocalItem.LocalItemType>> deletedItems;
///////////////////////////////////////////////////////////////////////////
// Fragment LifeCycle - Creation
@@ -86,11 +65,6 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
localPlaylistManager = new LocalPlaylistManager(database);
remotePlaylistManager = new RemotePlaylistManager(database);
disposables = new CompositeDisposable();
isLoadingComplete = new AtomicBoolean();
debounceSaver = new DebounceSaver(3000, this);
deletedItems = new ArrayList<>();
}
@Nullable
@@ -117,20 +91,10 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
// Fragment LifeCycle - Views
///////////////////////////////////////////////////////////////////////////
@Override
protected void initViews(final View rootView, final Bundle savedInstanceState) {
super.initViews(rootView, savedInstanceState);
itemListAdapter.setUseItemHandle(true);
}
@Override
protected void initListeners() {
super.initListeners();
itemTouchHelper = new ItemTouchHelper(getItemTouchCallback());
itemTouchHelper.attachToRecyclerView(itemsList);
itemListAdapter.setSelectedListener(new OnClickGesture<>() {
@Override
public void selected(final LocalItem selectedItem) {
@@ -138,7 +102,7 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
if (selectedItem instanceof PlaylistMetadataEntry) {
final PlaylistMetadataEntry entry = ((PlaylistMetadataEntry) selectedItem);
NavigationHelper.openLocalPlaylistFragment(fragmentManager, entry.getUid(),
NavigationHelper.openLocalPlaylistFragment(fragmentManager, entry.uid,
entry.name);
} else if (selectedItem instanceof PlaylistRemoteEntity) {
@@ -159,14 +123,6 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
showRemoteDeleteDialog((PlaylistRemoteEntity) selectedItem);
}
}
@Override
public void drag(final LocalItem selectedItem,
final RecyclerView.ViewHolder viewHolder) {
if (itemTouchHelper != null) {
itemTouchHelper.startDrag(viewHolder);
}
}
});
}
@@ -178,13 +134,8 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
public void startLoading(final boolean forceLoad) {
super.startLoading(forceLoad);
if (debounceSaver != null) {
disposables.add(debounceSaver.getDebouncedSaver());
debounceSaver.setNoChangesToSave();
}
isLoadingComplete.set(false);
getMergedOrderedPlaylists(localPlaylistManager, remotePlaylistManager)
Flowable.combineLatest(localPlaylistManager.getPlaylists(),
remotePlaylistManager.getPlaylists(), PlaylistLocalItem::merge)
.onBackpressureLatest()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(getPlaylistsSubscriber());
@@ -198,9 +149,6 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
public void onPause() {
super.onPause();
itemsListState = itemsList.getLayoutManager().onSaveInstanceState();
// Save on exit
saveImmediate();
}
@Override
@@ -215,27 +163,19 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
}
databaseSubscription = null;
itemTouchHelper = null;
}
@Override
public void onDestroy() {
super.onDestroy();
if (debounceSaver != null) {
debounceSaver.getDebouncedSaveSignal().onComplete();
}
if (disposables != null) {
disposables.dispose();
}
debounceSaver = null;
disposables = null;
localPlaylistManager = null;
remotePlaylistManager = null;
itemsListState = null;
isLoadingComplete = null;
deletedItems = null;
}
///////////////////////////////////////////////////////////////////////////
@@ -243,12 +183,10 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
///////////////////////////////////////////////////////////////////////////
private Subscriber<List<PlaylistLocalItem>> getPlaylistsSubscriber() {
return new Subscriber<>() {
return new Subscriber<List<PlaylistLocalItem>>() {
@Override
public void onSubscribe(final Subscription s) {
showLoading();
isLoadingComplete.set(false);
if (databaseSubscription != null) {
databaseSubscription.cancel();
}
@@ -258,10 +196,7 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
@Override
public void onNext(final List<PlaylistLocalItem> subscriptions) {
if (debounceSaver == null || !debounceSaver.getIsModified()) {
handleResult(subscriptions);
isLoadingComplete.set(true);
}
handleResult(subscriptions);
if (databaseSubscription != null) {
databaseSubscription.request(1);
}
@@ -274,8 +209,7 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
}
@Override
public void onComplete() {
}
public void onComplete() { }
};
}
@@ -310,183 +244,12 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
}
}
/*//////////////////////////////////////////////////////////////////////////
// Playlist Metadata Manipulation
//////////////////////////////////////////////////////////////////////////*/
private void changeLocalPlaylistName(final long id, final String name) {
if (localPlaylistManager == null) {
return;
}
if (DEBUG) {
Log.d(TAG, "Updating playlist id=[" + id + "] "
+ "with new name=[" + name + "] items");
}
final Disposable disposable = localPlaylistManager.renamePlaylist(id, name)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(longs -> { /*Do nothing on success*/ }, throwable -> showError(
new ErrorInfo(throwable,
UserAction.REQUESTED_BOOKMARK,
"Changing playlist name")));
disposables.add(disposable);
}
private void deleteItem(final PlaylistLocalItem item) {
if (itemListAdapter == null) {
return;
}
itemListAdapter.removeItem(item);
if (item instanceof PlaylistMetadataEntry) {
deletedItems.add(new Pair<>(item.getUid(),
LocalItem.LocalItemType.PLAYLIST_LOCAL_ITEM));
} else if (item instanceof PlaylistRemoteEntity) {
deletedItems.add(new Pair<>(item.getUid(),
LocalItem.LocalItemType.PLAYLIST_REMOTE_ITEM));
}
if (debounceSaver != null) {
debounceSaver.setHasChangesToSave();
saveImmediate();
}
}
@Override
public void saveImmediate() {
if (itemListAdapter == null) {
return;
}
// List must be loaded and modified in order to save
if (isLoadingComplete == null || debounceSaver == null
|| !isLoadingComplete.get() || !debounceSaver.getIsModified()) {
return;
}
final List<LocalItem> items = itemListAdapter.getItemsList();
final List<PlaylistMetadataEntry> localItemsUpdate = new ArrayList<>();
final List<Long> localItemsDeleteUid = new ArrayList<>();
final List<PlaylistRemoteEntity> remoteItemsUpdate = new ArrayList<>();
final List<Long> remoteItemsDeleteUid = new ArrayList<>();
// Calculate display index
for (int i = 0; i < items.size(); i++) {
final LocalItem item = items.get(i);
if (item instanceof PlaylistMetadataEntry
&& ((PlaylistMetadataEntry) item).getDisplayIndex() != i) {
((PlaylistMetadataEntry) item).setDisplayIndex(i);
localItemsUpdate.add((PlaylistMetadataEntry) item);
} else if (item instanceof PlaylistRemoteEntity
&& ((PlaylistRemoteEntity) item).getDisplayIndex() != i) {
((PlaylistRemoteEntity) item).setDisplayIndex(i);
remoteItemsUpdate.add((PlaylistRemoteEntity) item);
}
}
// Find deleted items
for (final Pair<Long, LocalItem.LocalItemType> item : deletedItems) {
if (item.second.equals(LocalItem.LocalItemType.PLAYLIST_LOCAL_ITEM)) {
localItemsDeleteUid.add(item.first);
} else if (item.second.equals(LocalItem.LocalItemType.PLAYLIST_REMOTE_ITEM)) {
remoteItemsDeleteUid.add(item.first);
}
}
deletedItems.clear();
// 1. Update local playlists
// 2. Update remote playlists
// 3. Set NoChangesToSave
disposables.add(localPlaylistManager.updatePlaylists(localItemsUpdate, localItemsDeleteUid)
.mergeWith(remotePlaylistManager.updatePlaylists(
remoteItemsUpdate, remoteItemsDeleteUid))
.observeOn(AndroidSchedulers.mainThread())
.subscribe(() -> {
if (debounceSaver != null) {
debounceSaver.setNoChangesToSave();
}
},
throwable -> showError(new ErrorInfo(throwable,
UserAction.REQUESTED_BOOKMARK, "Saving playlist"))
));
}
private ItemTouchHelper.SimpleCallback getItemTouchCallback() {
// if adding grid layout, also include ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT
// with an `if (shouldUseGridLayout()) ...`
return new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN,
ItemTouchHelper.ACTION_STATE_IDLE) {
@Override
public int interpolateOutOfBoundsScroll(@NonNull final RecyclerView recyclerView,
final int viewSize,
final int viewSizeOutOfBounds,
final int totalSize,
final long msSinceStartScroll) {
final int standardSpeed = super.interpolateOutOfBoundsScroll(recyclerView,
viewSize, viewSizeOutOfBounds, totalSize, msSinceStartScroll);
final int minimumAbsVelocity = Math.max(MINIMUM_INITIAL_DRAG_VELOCITY,
Math.abs(standardSpeed));
return minimumAbsVelocity * (int) Math.signum(viewSizeOutOfBounds);
}
@Override
public boolean onMove(@NonNull final RecyclerView recyclerView,
@NonNull final RecyclerView.ViewHolder source,
@NonNull final RecyclerView.ViewHolder target) {
// Allow swap LocalBookmarkPlaylistItemHolder and RemoteBookmarkPlaylistItemHolder.
if (itemListAdapter == null
|| source.getItemViewType() != target.getItemViewType()
&& !(
(
(source instanceof LocalBookmarkPlaylistItemHolder)
|| (source instanceof RemoteBookmarkPlaylistItemHolder)
)
&& (
(target instanceof LocalBookmarkPlaylistItemHolder)
|| (target instanceof RemoteBookmarkPlaylistItemHolder)
))
) {
return false;
}
final int sourceIndex = source.getBindingAdapterPosition();
final int targetIndex = target.getBindingAdapterPosition();
final boolean isSwapped = itemListAdapter.swapItems(sourceIndex, targetIndex);
if (isSwapped && debounceSaver != null) {
debounceSaver.setHasChangesToSave();
}
return isSwapped;
}
@Override
public boolean isLongPressDragEnabled() {
return false;
}
@Override
public boolean isItemViewSwipeEnabled() {
return false;
}
@Override
public void onSwiped(@NonNull final RecyclerView.ViewHolder viewHolder,
final int swipeDir) {
// Do nothing.
}
};
}
///////////////////////////////////////////////////////////////////////////
// Utils
///////////////////////////////////////////////////////////////////////////
private void showRemoteDeleteDialog(final PlaylistRemoteEntity item) {
showDeleteDialog(item.getName(), item);
showDeleteDialog(item.getName(), remotePlaylistManager.deletePlaylist(item.getUid()));
}
private void showLocalDialog(final PlaylistMetadataEntry selectedItem) {
@@ -494,7 +257,7 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
final String delete = getString(R.string.delete);
final String unsetThumbnail = getString(R.string.unset_playlist_thumbnail);
final boolean isThumbnailPermanent = localPlaylistManager
.getIsPlaylistThumbnailPermanent(selectedItem.getUid());
.getIsPlaylistThumbnailPermanent(selectedItem.uid);
final ArrayList<String> items = new ArrayList<>();
items.add(rename);
@@ -507,12 +270,13 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
if (items.get(index).equals(rename)) {
showRenameDialog(selectedItem);
} else if (items.get(index).equals(delete)) {
showDeleteDialog(selectedItem.name, selectedItem);
showDeleteDialog(selectedItem.name,
localPlaylistManager.deletePlaylist(selectedItem.uid));
} else if (isThumbnailPermanent && items.get(index).equals(unsetThumbnail)) {
final long thumbnailStreamId = localPlaylistManager
.getAutomaticPlaylistThumbnailStreamId(selectedItem.getUid());
.getAutomaticPlaylistThumbnailStreamId(selectedItem.uid);
localPlaylistManager
.changePlaylistThumbnail(selectedItem.getUid(), thumbnailStreamId, false)
.changePlaylistThumbnail(selectedItem.uid, thumbnailStreamId, false)
.observeOn(AndroidSchedulers.mainThread())
.subscribe();
}
@@ -534,13 +298,13 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
.setView(dialogBinding.getRoot())
.setPositiveButton(R.string.rename_playlist, (dialog, which) ->
changeLocalPlaylistName(
selectedItem.getUid(),
selectedItem.uid,
dialogBinding.dialogEditText.getText().toString()))
.setNegativeButton(R.string.cancel, null)
.show();
}
private void showDeleteDialog(final String name, final PlaylistLocalItem item) {
private void showDeleteDialog(final String name, final Single<Integer> deleteReactor) {
if (activity == null || disposables == null) {
return;
}
@@ -549,8 +313,35 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
.setTitle(name)
.setMessage(R.string.delete_playlist_prompt)
.setCancelable(true)
.setPositiveButton(R.string.delete, (dialog, i) -> deleteItem(item))
.setPositiveButton(R.string.delete, (dialog, i) ->
disposables.add(deleteReactor
.observeOn(AndroidSchedulers.mainThread())
.subscribe(ignored -> { /*Do nothing on success*/ }, throwable ->
showError(new ErrorInfo(throwable,
UserAction.REQUESTED_BOOKMARK,
"Deleting playlist")))))
.setNegativeButton(R.string.cancel, null)
.show();
}
private void changeLocalPlaylistName(final long id, final String name) {
if (localPlaylistManager == null) {
return;
}
if (DEBUG) {
Log.d(TAG, "Updating playlist id=[" + id + "] "
+ "with new name=[" + name + "] items");
}
localPlaylistManager.renamePlaylist(id, name);
final Disposable disposable = localPlaylistManager.renamePlaylist(id, name)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(longs -> { /*Do nothing on success*/ }, throwable -> showError(
new ErrorInfo(throwable,
UserAction.REQUESTED_BOOKMARK,
"Changing playlist name")));
disposables.add(disposable);
}
}

View File

@@ -1,95 +0,0 @@
package org.schabi.newpipe.local.bookmark;
import org.schabi.newpipe.database.playlist.PlaylistLocalItem;
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import io.reactivex.rxjava3.core.Flowable;
/**
* Takes care of remote and local playlists at once, hence "merged".
*/
public final class MergedPlaylistManager {
private MergedPlaylistManager() {
}
public static Flowable<List<PlaylistLocalItem>> getMergedOrderedPlaylists(
final LocalPlaylistManager localPlaylistManager,
final RemotePlaylistManager remotePlaylistManager) {
return Flowable.combineLatest(
localPlaylistManager.getPlaylists(),
remotePlaylistManager.getPlaylists(),
MergedPlaylistManager::merge
);
}
/**
* Merge localPlaylists and remotePlaylists by the display index.
* If two items have the same display index, sort them in {@code CASE_INSENSITIVE_ORDER}.
*
* @param localPlaylists local playlists, already sorted by display index
* @param remotePlaylists remote playlists, already sorted by display index
* @return merged playlists
*/
public static List<PlaylistLocalItem> merge(
final List<PlaylistMetadataEntry> localPlaylists,
final List<PlaylistRemoteEntity> remotePlaylists) {
// This algorithm is similar to the merge operation in merge sort.
final List<PlaylistLocalItem> result = new ArrayList<>(
localPlaylists.size() + remotePlaylists.size());
final List<PlaylistLocalItem> itemsWithSameIndex = new ArrayList<>();
int i = 0;
int j = 0;
while (i < localPlaylists.size()) {
while (j < remotePlaylists.size()) {
if (remotePlaylists.get(j).getDisplayIndex()
<= localPlaylists.get(i).getDisplayIndex()) {
addItem(result, remotePlaylists.get(j), itemsWithSameIndex);
j++;
} else {
break;
}
}
addItem(result, localPlaylists.get(i), itemsWithSameIndex);
i++;
}
while (j < remotePlaylists.size()) {
addItem(result, remotePlaylists.get(j), itemsWithSameIndex);
j++;
}
addItemsWithSameIndex(result, itemsWithSameIndex);
return result;
}
private static void addItem(final List<PlaylistLocalItem> result,
final PlaylistLocalItem item,
final List<PlaylistLocalItem> itemsWithSameIndex) {
if (!itemsWithSameIndex.isEmpty()
&& itemsWithSameIndex.get(0).getDisplayIndex() != item.getDisplayIndex()) {
// The new item has a different display index, add previous items with same
// index to the result.
addItemsWithSameIndex(result, itemsWithSameIndex);
itemsWithSameIndex.clear();
}
itemsWithSameIndex.add(item);
}
private static void addItemsWithSameIndex(final List<PlaylistLocalItem> result,
final List<PlaylistLocalItem> itemsWithSameIndex) {
Collections.sort(itemsWithSameIndex,
Comparator.comparing(PlaylistLocalItem::getOrderingName,
Comparator.nullsLast(String.CASE_INSENSITIVE_ORDER)));
result.addAll(itemsWithSameIndex);
}
}

View File

@@ -155,14 +155,14 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
final Toast successToast = Toast.makeText(getContext(), toastText, Toast.LENGTH_SHORT);
playlistDisposables.add(manager.appendToPlaylist(playlist.getUid(), streams)
playlistDisposables.add(manager.appendToPlaylist(playlist.uid, streams)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(ignored -> {
successToast.show();
if (playlist.thumbnailUrl.equals(PlaylistEntity.DEFAULT_THUMBNAIL)) {
playlistDisposables.add(manager
.changePlaylistThumbnail(playlist.getUid(), streams.get(0).getUid(),
.changePlaylistThumbnail(playlist.uid, streams.get(0).getUid(),
false)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(ignore -> successToast.show()));

View File

@@ -18,7 +18,7 @@ data class FeedUpdateInfo(
@NotificationMode
val notificationMode: Int,
val name: String,
val avatarUrl: String?,
val avatarUrl: String,
val url: String,
val serviceId: Int,
// description and subscriberCount are null if the constructor info is from the fast feed method

View File

@@ -1,54 +0,0 @@
package org.schabi.newpipe.local.holder;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
import org.schabi.newpipe.local.LocalItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import java.time.format.DateTimeFormatter;
public class LocalBookmarkPlaylistItemHolder extends LocalPlaylistItemHolder {
private final View itemHandleView;
public LocalBookmarkPlaylistItemHolder(final LocalItemBuilder infoItemBuilder,
final ViewGroup parent) {
this(infoItemBuilder, R.layout.list_playlist_bookmark_item, parent);
}
LocalBookmarkPlaylistItemHolder(final LocalItemBuilder infoItemBuilder, final int layoutId,
final ViewGroup parent) {
super(infoItemBuilder, layoutId, parent);
itemHandleView = itemView.findViewById(R.id.itemHandle);
}
@Override
public void updateFromItem(final LocalItem localItem,
final HistoryRecordManager historyRecordManager,
final DateTimeFormatter dateTimeFormatter) {
if (!(localItem instanceof PlaylistMetadataEntry)) {
return;
}
final PlaylistMetadataEntry item = (PlaylistMetadataEntry) localItem;
itemHandleView.setOnTouchListener(getOnTouchListener(item));
super.updateFromItem(localItem, historyRecordManager, dateTimeFormatter);
}
private View.OnTouchListener getOnTouchListener(final PlaylistMetadataEntry item) {
return (view, motionEvent) -> {
view.performClick();
if (itemBuilder != null && itemBuilder.getOnItemSelectedListener() != null
&& motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) {
itemBuilder.getOnItemSelectedListener().drag(item,
LocalBookmarkPlaylistItemHolder.this);
}
return false;
};
}
}

View File

@@ -1,54 +0,0 @@
package org.schabi.newpipe.local.holder;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
import org.schabi.newpipe.local.LocalItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import java.time.format.DateTimeFormatter;
public class RemoteBookmarkPlaylistItemHolder extends RemotePlaylistItemHolder {
private final View itemHandleView;
public RemoteBookmarkPlaylistItemHolder(final LocalItemBuilder infoItemBuilder,
final ViewGroup parent) {
this(infoItemBuilder, R.layout.list_playlist_bookmark_item, parent);
}
RemoteBookmarkPlaylistItemHolder(final LocalItemBuilder infoItemBuilder, final int layoutId,
final ViewGroup parent) {
super(infoItemBuilder, layoutId, parent);
itemHandleView = itemView.findViewById(R.id.itemHandle);
}
@Override
public void updateFromItem(final LocalItem localItem,
final HistoryRecordManager historyRecordManager,
final DateTimeFormatter dateTimeFormatter) {
if (!(localItem instanceof PlaylistRemoteEntity)) {
return;
}
final PlaylistRemoteEntity item = (PlaylistRemoteEntity) localItem;
itemHandleView.setOnTouchListener(getOnTouchListener(item));
super.updateFromItem(localItem, historyRecordManager, dateTimeFormatter);
}
private View.OnTouchListener getOnTouchListener(final PlaylistRemoteEntity item) {
return (view, motionEvent) -> {
view.performClick();
if (itemBuilder != null && itemBuilder.getOnItemSelectedListener() != null
&& motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) {
itemBuilder.getOnItemSelectedListener().drag(item,
RemoteBookmarkPlaylistItemHolder.this);
}
return false;
};
}
}

View File

@@ -14,7 +14,6 @@ import org.schabi.newpipe.util.ServiceHelper;
import java.time.format.DateTimeFormatter;
public class RemotePlaylistItemHolder extends PlaylistItemHolder {
public RemotePlaylistItemHolder(final LocalItemBuilder infoItemBuilder,
final ViewGroup parent) {
super(infoItemBuilder, parent);

View File

@@ -49,8 +49,6 @@ import org.schabi.newpipe.local.BaseLocalListFragment;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
import org.schabi.newpipe.util.debounce.DebounceSavable;
import org.schabi.newpipe.util.debounce.DebounceSaver;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.OnClickGesture;
@@ -60,6 +58,7 @@ import org.schabi.newpipe.util.external_communication.ShareUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;
@@ -69,10 +68,12 @@ import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.schedulers.Schedulers;
import io.reactivex.rxjava3.subjects.PublishSubject;
public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistStreamEntry>, Void>
implements PlaylistControlViewHolder, DebounceSavable {
implements PlaylistControlViewHolder {
/** Save the list 10 seconds after the last change occurred. */
private static final long SAVE_DEBOUNCE_MILLIS = 10000;
private static final int MINIMUM_INITIAL_DRAG_VELOCITY = 12;
@State
protected Long playlistId;
@@ -89,12 +90,13 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
private LocalPlaylistManager playlistManager;
private Subscription databaseSubscription;
private PublishSubject<Long> debouncedSaveSignal;
private CompositeDisposable disposables;
/** Whether the playlist has been fully loaded from db. */
private AtomicBoolean isLoadingComplete;
/** Used to debounce saving playlist edits to disk. */
private DebounceSaver debounceSaver;
/** Whether the playlist has been modified (e.g. items reordered or deleted) */
private AtomicBoolean isModified;
/** Flag to prevent simultaneous rewrites of the playlist. */
private boolean isRewritingPlaylist = false;
@@ -119,11 +121,12 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
playlistManager = new LocalPlaylistManager(NewPipeDatabase.getInstance(requireContext()));
debouncedSaveSignal = PublishSubject.create();
disposables = new CompositeDisposable();
isLoadingComplete = new AtomicBoolean();
debounceSaver = new DebounceSaver(this);
isModified = new AtomicBoolean();
}
@Override
@@ -163,6 +166,17 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
return headerBinding;
}
/**
* <p>Commit changes immediately if the playlist has been modified.</p>
* Delete operations and other modifications will be committed to ensure that the database
* is up to date, e.g. when the user adds the just deleted stream from another fragment.
*/
public void commitChanges() {
if (isModified != null && isModified.get()) {
saveImmediate();
}
}
@Override
protected void initListeners() {
super.initListeners();
@@ -229,13 +243,10 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
if (disposables != null) {
disposables.clear();
}
if (debounceSaver != null) {
disposables.add(debounceSaver.getDebouncedSaver());
debounceSaver.setNoChangesToSave();
}
disposables.add(getDebouncedSaver());
isLoadingComplete.set(false);
isModified.set(false);
playlistManager.getPlaylistStreams(playlistId)
.onBackpressureLatest()
@@ -293,8 +304,8 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
@Override
public void onDestroy() {
super.onDestroy();
if (debounceSaver != null) {
debounceSaver.getDebouncedSaveSignal().onComplete();
if (debouncedSaveSignal != null) {
debouncedSaveSignal.onComplete();
}
if (disposables != null) {
disposables.dispose();
@@ -303,11 +314,12 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
tabsPagerAdapter.getLocalPlaylistFragments().remove(this);
}
debounceSaver = null;
debouncedSaveSignal = null;
playlistManager = null;
disposables = null;
isLoadingComplete = null;
isModified = null;
}
///////////////////////////////////////////////////////////////////////////
@@ -331,7 +343,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
@Override
public void onNext(final List<PlaylistStreamEntry> streams) {
// Skip handling the result after it has been modified
if (debounceSaver == null || !debounceSaver.getIsModified()) {
if (isModified == null || !isModified.get()) {
handleResult(streams);
isLoadingComplete.set(true);
}
@@ -483,7 +495,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
itemListAdapter.clearStreamItemList();
itemListAdapter.addItems(itemsToKeep);
debounceSaver.setHasChangesToSave();
saveChanges();
if (thumbnailVideoRemoved) {
updateThumbnailUrl();
@@ -654,7 +666,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
itemListAdapter.clearStreamItemList();
itemListAdapter.addItems(itemsToKeep);
setStreamCountAndOverallDuration(itemListAdapter.getItemsList());
debounceSaver.setHasChangesToSave();
saveChanges();
hideLoading();
isRewritingPlaylist = false;
@@ -673,23 +685,41 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
}
setStreamCountAndOverallDuration(itemListAdapter.getItemsList());
debounceSaver.setHasChangesToSave();
saveChanges();
}
/**
* <p>Commit changes immediately if the playlist has been modified.</p>
* Delete operations and other modifications will be committed to ensure that the database
* is up to date, e.g. when the user adds the just deleted stream from another fragment.
*/
@Override
public void saveImmediate() {
private void saveChanges() {
if (isModified == null || debouncedSaveSignal == null) {
return;
}
isModified.set(true);
debouncedSaveSignal.onNext(System.currentTimeMillis());
}
private Disposable getDebouncedSaver() {
if (debouncedSaveSignal == null) {
return Disposable.empty();
}
return debouncedSaveSignal
.debounce(SAVE_DEBOUNCE_MILLIS, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(ignored -> saveImmediate(), throwable ->
showError(new ErrorInfo(throwable, UserAction.SOMETHING_ELSE,
"Debounced saver")));
}
private void saveImmediate() {
if (playlistManager == null || itemListAdapter == null) {
return;
}
// List must be loaded and modified in order to save
if (isLoadingComplete == null || debounceSaver == null
|| !isLoadingComplete.get() || !debounceSaver.getIsModified()) {
if (isLoadingComplete == null || isModified == null
|| !isLoadingComplete.get() || !isModified.get()) {
Log.w(TAG, "Attempting to save playlist when local playlist "
+ "is not loaded or not modified: playlist id=[" + playlistId + "]");
return;
}
@@ -710,8 +740,8 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
() -> {
if (debounceSaver != null) {
debounceSaver.setNoChangesToSave();
if (isModified != null) {
isModified.set(false);
}
},
throwable -> showError(new ErrorInfo(throwable,
@@ -754,7 +784,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
final int targetIndex = target.getBindingAdapterPosition();
final boolean isSwapped = itemListAdapter.swapItems(sourceIndex, targetIndex);
if (isSwapped) {
debounceSaver.setHasChangesToSave();
saveChanges();
}
return isSwapped;
}
@@ -837,8 +867,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
headerBinding.playlistStreamCount.setText(
Localization.concatenateStrings(
Localization.localizeStreamCount(activity, streamCount),
Localization.getDurationString(playlistOverallDurationSeconds,
true, true))
Localization.getDurationString(playlistOverallDurationSeconds))
);
}
}

View File

@@ -19,6 +19,7 @@ import java.util.List;
import io.reactivex.rxjava3.core.Completable;
import io.reactivex.rxjava3.core.Flowable;
import io.reactivex.rxjava3.core.Maybe;
import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.schedulers.Schedulers;
public class LocalPlaylistManager {
@@ -42,13 +43,10 @@ public class LocalPlaylistManager {
return Maybe.empty();
}
// Save to the database directly.
// Make sure the new playlist is always on the top of bookmark.
// The index will be reassigned to non-negative number in BookmarkFragment.
return Maybe.fromCallable(() -> database.runInTransaction(() -> {
final List<Long> streamIds = streamTable.upsertAll(streams);
final PlaylistEntity newPlaylist = new PlaylistEntity(name, false,
streamIds.get(0), -1);
streamIds.get(0));
return insertJoinEntities(playlistTable.insert(newPlaylist),
streamIds, 0);
@@ -91,20 +89,8 @@ public class LocalPlaylistManager {
})).subscribeOn(Schedulers.io());
}
public Completable updatePlaylists(final List<PlaylistMetadataEntry> updateItems,
final List<Long> deletedItems) {
final List<PlaylistEntity> items = new ArrayList<>(updateItems.size());
for (final PlaylistMetadataEntry item : updateItems) {
items.add(new PlaylistEntity(item));
}
return Completable.fromRunnable(() -> database.runInTransaction(() -> {
for (final Long uid : deletedItems) {
playlistTable.deletePlaylist(uid);
}
for (final PlaylistEntity item : items) {
playlistTable.upsertPlaylist(item);
}
})).subscribeOn(Schedulers.io());
public Flowable<List<PlaylistMetadataEntry>> getPlaylists() {
return playlistStreamTable.getPlaylistMetadata().subscribeOn(Schedulers.io());
}
public Flowable<List<PlaylistStreamEntry>> getDistinctPlaylistStreams(final long playlistId) {
@@ -124,14 +110,15 @@ public class LocalPlaylistManager {
.subscribeOn(Schedulers.io());
}
public Flowable<List<PlaylistMetadataEntry>> getPlaylists() {
return playlistStreamTable.getPlaylistMetadata().subscribeOn(Schedulers.io());
}
public Flowable<List<PlaylistStreamEntry>> getPlaylistStreams(final long playlistId) {
return playlistStreamTable.getOrderedStreamsOf(playlistId).subscribeOn(Schedulers.io());
}
public Single<Integer> deletePlaylist(final long playlistId) {
return Single.fromCallable(() -> playlistTable.deletePlaylist(playlistId))
.subscribeOn(Schedulers.io());
}
public Maybe<Integer> renamePlaylist(final long playlistId, final String name) {
return modifyPlaylist(playlistId, name, THUMBNAIL_ID_LEAVE_UNCHANGED, false);
}

View File

@@ -7,23 +7,20 @@ import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
import java.util.List;
import io.reactivex.rxjava3.core.Completable;
import io.reactivex.rxjava3.core.Flowable;
import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.schedulers.Schedulers;
public class RemotePlaylistManager {
private final AppDatabase database;
private final PlaylistRemoteDAO playlistRemoteTable;
public RemotePlaylistManager(final AppDatabase db) {
database = db;
playlistRemoteTable = db.playlistRemoteDAO();
}
public Flowable<List<PlaylistRemoteEntity>> getPlaylists() {
return playlistRemoteTable.getPlaylists().subscribeOn(Schedulers.io());
return playlistRemoteTable.getAll().subscribeOn(Schedulers.io());
}
public Flowable<List<PlaylistRemoteEntity>> getPlaylist(final PlaylistInfo info) {
@@ -36,18 +33,6 @@ public class RemotePlaylistManager {
.subscribeOn(Schedulers.io());
}
public Completable updatePlaylists(final List<PlaylistRemoteEntity> updateItems,
final List<Long> deletedItems) {
return Completable.fromRunnable(() -> database.runInTransaction(() -> {
for (final Long uid: deletedItems) {
playlistRemoteTable.deletePlaylist(uid);
}
for (final PlaylistRemoteEntity item: updateItems) {
playlistRemoteTable.upsert(item);
}
})).subscribeOn(Schedulers.io());
}
public Single<Long> onBookmark(final PlaylistInfo playlistInfo) {
return Single.fromCallable(() -> {
final PlaylistRemoteEntity playlist = new PlaylistRemoteEntity(playlistInfo);

View File

@@ -100,9 +100,7 @@ class SubscriptionManager(context: Context) {
val subscriptionEntity = subscriptionTable.getSubscription(info.uid)
subscriptionEntity.name = info.name
// some services do not provide an avatar URL
info.avatarUrl?.let { subscriptionEntity.avatarUrl = it }
subscriptionEntity.avatarUrl = info.avatarUrl
// these two fields are null if the feed info was fetched using the fast feed method
info.description?.let { subscriptionEntity.description = it }

View File

@@ -21,15 +21,9 @@ import androidx.core.content.ContextCompat;
import androidx.preference.Preference;
import androidx.preference.PreferenceManager;
import com.grack.nanojson.JsonParserException;
import org.schabi.newpipe.NewPipeDatabase;
import org.schabi.newpipe.R;
import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.ErrorUtil;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.settings.export.BackupFileLocator;
import org.schabi.newpipe.settings.export.ImportExportManager;
import org.schabi.newpipe.streams.io.NoFileManagerSafeGuard;
import org.schabi.newpipe.streams.io.StoredFileHelper;
import org.schabi.newpipe.util.NavigationHelper;
@@ -48,7 +42,7 @@ public class BackupRestoreSettingsFragment extends BasePreferenceFragment {
private final SimpleDateFormat exportDateFormat =
new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US);
private ImportExportManager manager;
private ContentSettingsManager manager;
private String importExportDataPathKey;
private final ActivityResultLauncher<Intent> requestImportPathLauncher =
registerForActivityResult(new ActivityResultContracts.StartActivityForResult(),
@@ -63,7 +57,8 @@ public class BackupRestoreSettingsFragment extends BasePreferenceFragment {
@Nullable final String rootKey) {
final File homeDir = ContextCompat.getDataDir(requireContext());
Objects.requireNonNull(homeDir);
manager = new ImportExportManager(new BackupFileLocator(homeDir));
manager = new ContentSettingsManager(new NewPipeFileLocator(homeDir));
manager.deleteSettingsFile();
importExportDataPathKey = getString(R.string.import_export_data_path);
@@ -170,7 +165,7 @@ public class BackupRestoreSettingsFragment extends BasePreferenceFragment {
Toast.makeText(requireContext(), R.string.export_complete_toast, Toast.LENGTH_SHORT)
.show();
} catch (final Exception e) {
showErrorSnackbar(e, "Exporting database and settings");
ErrorUtil.showUiErrorSnackbar(this, "Exporting database", e);
}
}
@@ -187,7 +182,6 @@ public class BackupRestoreSettingsFragment extends BasePreferenceFragment {
throw new IOException("Could not create databases dir");
}
// replace the current database
if (!manager.extractDb(file)) {
Toast.makeText(requireContext(), R.string.could_not_import_all_files,
Toast.LENGTH_LONG)
@@ -195,13 +189,9 @@ public class BackupRestoreSettingsFragment extends BasePreferenceFragment {
}
// if settings file exist, ask if it should be imported.
final boolean hasJsonPrefs = manager.exportHasJsonPrefs(file);
if (hasJsonPrefs || manager.exportHasSerializedPrefs(file)) {
if (manager.extractSettings(file)) {
new androidx.appcompat.app.AlertDialog.Builder(requireContext())
.setTitle(R.string.import_settings)
.setMessage(hasJsonPrefs ? null : requireContext()
.getString(R.string.import_settings_vulnerable_format))
.setOnDismissListener(dialog -> finishImport(importDataUri))
.setNegativeButton(R.string.cancel, (dialog, which) -> {
dialog.dismiss();
finishImport(importDataUri);
@@ -211,16 +201,7 @@ public class BackupRestoreSettingsFragment extends BasePreferenceFragment {
final Context context = requireContext();
final SharedPreferences prefs = PreferenceManager
.getDefaultSharedPreferences(context);
try {
if (hasJsonPrefs) {
manager.loadJsonPrefs(file, prefs);
} else {
manager.loadSerializedPrefs(file, prefs);
}
} catch (IOException | ClassNotFoundException | JsonParserException e) {
createErrorNotification(e, "Importing preferences");
return;
}
manager.loadSharedPreferences(prefs);
cleanImport(context, prefs);
finishImport(importDataUri);
})
@@ -229,7 +210,7 @@ public class BackupRestoreSettingsFragment extends BasePreferenceFragment {
finishImport(importDataUri);
}
} catch (final Exception e) {
showErrorSnackbar(e, "Importing database and settings");
ErrorUtil.showUiErrorSnackbar(this, "Importing database", e);
}
}
@@ -266,7 +247,7 @@ public class BackupRestoreSettingsFragment extends BasePreferenceFragment {
}
/**
* Save import path and restart app.
* Save import path and restart system.
*
* @param importDataUri The import path to save
*/
@@ -287,15 +268,4 @@ public class BackupRestoreSettingsFragment extends BasePreferenceFragment {
.putString(importExportDataPathKey, importExportDataUri.toString());
editor.apply();
}
private void showErrorSnackbar(final Throwable e, final String request) {
ErrorUtil.showSnackbar(this, new ErrorInfo(e, UserAction.DATABASE_IMPORT_EXPORT, request));
}
private void createErrorNotification(final Throwable e, final String request) {
ErrorUtil.createNotification(
requireContext(),
new ErrorInfo(e, UserAction.DATABASE_IMPORT_EXPORT, request)
);
}
}

View File

@@ -0,0 +1,120 @@
package org.schabi.newpipe.settings
import android.content.SharedPreferences
import android.util.Log
import org.schabi.newpipe.MainActivity.DEBUG
import org.schabi.newpipe.streams.io.SharpOutputStream
import org.schabi.newpipe.streams.io.StoredFileHelper
import org.schabi.newpipe.util.ZipHelper
import java.io.IOException
import java.io.ObjectInputStream
import java.io.ObjectOutputStream
import java.util.zip.ZipOutputStream
class ContentSettingsManager(private val fileLocator: NewPipeFileLocator) {
companion object {
const val TAG = "ContentSetManager"
}
/**
* Exports given [SharedPreferences] to the file in given outputPath.
* It also creates the file.
*/
@Throws(Exception::class)
fun exportDatabase(preferences: SharedPreferences, file: StoredFileHelper) {
file.create()
ZipOutputStream(SharpOutputStream(file.stream).buffered())
.use { outZip ->
ZipHelper.addFileToZip(outZip, fileLocator.db.path, "newpipe.db")
try {
ObjectOutputStream(fileLocator.settings.outputStream()).use { output ->
output.writeObject(preferences.all)
output.flush()
}
} catch (e: IOException) {
if (DEBUG) {
Log.e(TAG, "Unable to exportDatabase", e)
}
}
ZipHelper.addFileToZip(outZip, fileLocator.settings.path, "newpipe.settings")
}
}
fun deleteSettingsFile() {
fileLocator.settings.delete()
}
/**
* Tries to create database directory if it does not exist.
*
* @return Whether the directory exists afterwards.
*/
fun ensureDbDirectoryExists(): Boolean {
return fileLocator.dbDir.exists() || fileLocator.dbDir.mkdir()
}
fun extractDb(file: StoredFileHelper): Boolean {
val success = ZipHelper.extractFileFromZip(file, fileLocator.db.path, "newpipe.db")
if (success) {
fileLocator.dbJournal.delete()
fileLocator.dbWal.delete()
fileLocator.dbShm.delete()
}
return success
}
fun extractSettings(file: StoredFileHelper): Boolean {
return ZipHelper.extractFileFromZip(file, fileLocator.settings.path, "newpipe.settings")
}
/**
* Remove all shared preferences from the app and load the preferences supplied to the manager.
*/
fun loadSharedPreferences(preferences: SharedPreferences) {
try {
val preferenceEditor = preferences.edit()
ObjectInputStream(fileLocator.settings.inputStream()).use { input ->
preferenceEditor.clear()
@Suppress("UNCHECKED_CAST")
val entries = input.readObject() as Map<String, *>
for ((key, value) in entries) {
when (value) {
is Boolean -> {
preferenceEditor.putBoolean(key, value)
}
is Float -> {
preferenceEditor.putFloat(key, value)
}
is Int -> {
preferenceEditor.putInt(key, value)
}
is Long -> {
preferenceEditor.putLong(key, value)
}
is String -> {
preferenceEditor.putString(key, value)
}
is Set<*> -> {
// There are currently only Sets with type String possible
@Suppress("UNCHECKED_CAST")
preferenceEditor.putStringSet(key, value as Set<String>?)
}
}
}
preferenceEditor.commit()
}
} catch (e: IOException) {
if (DEBUG) {
Log.e(TAG, "Unable to loadSharedPreferences", e)
}
} catch (e: ClassNotFoundException) {
if (DEBUG) {
Log.e(TAG, "Unable to loadSharedPreferences", e)
}
}
}
}

View File

@@ -0,0 +1,21 @@
package org.schabi.newpipe.settings
import java.io.File
/**
* Locates specific files of NewPipe based on the home directory of the app.
*/
class NewPipeFileLocator(private val homeDir: File) {
val dbDir by lazy { File(homeDir, "/databases") }
val db by lazy { File(homeDir, "/databases/newpipe.db") }
val dbJournal by lazy { File(homeDir, "/databases/newpipe.db-journal") }
val dbShm by lazy { File(homeDir, "/databases/newpipe.db-shm") }
val dbWal by lazy { File(homeDir, "/databases/newpipe.db-wal") }
val settings by lazy { File(homeDir, "/databases/newpipe.settings") }
}

View File

@@ -1,7 +1,5 @@
package org.schabi.newpipe.settings;
import static org.schabi.newpipe.local.bookmark.MergedPlaylistManager.getMergedOrderedPlaylists;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
@@ -33,6 +31,7 @@ import java.util.List;
import java.util.Vector;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Flowable;
import io.reactivex.rxjava3.disposables.Disposable;
public class SelectPlaylistFragment extends DialogFragment {
@@ -91,7 +90,8 @@ public class SelectPlaylistFragment extends DialogFragment {
final LocalPlaylistManager localPlaylistManager = new LocalPlaylistManager(database);
final RemotePlaylistManager remotePlaylistManager = new RemotePlaylistManager(database);
disposable = getMergedOrderedPlaylists(localPlaylistManager, remotePlaylistManager)
disposable = Flowable.combineLatest(localPlaylistManager.getPlaylists(),
remotePlaylistManager.getPlaylists(), PlaylistLocalItem::merge)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::displayPlaylists, this::onError);
}
@@ -118,7 +118,7 @@ public class SelectPlaylistFragment extends DialogFragment {
if (selectedItem instanceof PlaylistMetadataEntry) {
final PlaylistMetadataEntry entry = ((PlaylistMetadataEntry) selectedItem);
onSelectedListener.onLocalPlaylistSelected(entry.getUid(), entry.name);
onSelectedListener.onLocalPlaylistSelected(entry.uid, entry.name);
} else if (selectedItem instanceof PlaylistRemoteEntity) {
final PlaylistRemoteEntity entry = ((PlaylistRemoteEntity) selectedItem);

View File

@@ -1,28 +0,0 @@
package org.schabi.newpipe.settings.export
import java.io.File
/**
* Locates specific files of NewPipe based on the home directory of the app.
*/
class BackupFileLocator(private val homeDir: File) {
companion object {
const val FILE_NAME_DB = "newpipe.db"
@Deprecated(
"Serializing preferences with Java's ObjectOutputStream is vulnerable to injections",
replaceWith = ReplaceWith("FILE_NAME_JSON_PREFS")
)
const val FILE_NAME_SERIALIZED_PREFS = "newpipe.settings"
const val FILE_NAME_JSON_PREFS = "preferences.json"
}
val dbDir by lazy { File(homeDir, "/databases") }
val db by lazy { File(dbDir, FILE_NAME_DB) }
val dbJournal by lazy { File(dbDir, "$FILE_NAME_DB-journal") }
val dbShm by lazy { File(dbDir, "$FILE_NAME_DB-shm") }
val dbWal by lazy { File(dbDir, "$FILE_NAME_DB-wal") }
}

View File

@@ -1,180 +0,0 @@
package org.schabi.newpipe.settings.export
import android.content.SharedPreferences
import com.grack.nanojson.JsonArray
import com.grack.nanojson.JsonParser
import com.grack.nanojson.JsonParserException
import com.grack.nanojson.JsonWriter
import org.schabi.newpipe.streams.io.SharpOutputStream
import org.schabi.newpipe.streams.io.StoredFileHelper
import org.schabi.newpipe.util.ZipHelper
import java.io.FileNotFoundException
import java.io.IOException
import java.io.ObjectOutputStream
import java.util.zip.ZipOutputStream
class ImportExportManager(private val fileLocator: BackupFileLocator) {
companion object {
const val TAG = "ImportExportManager"
}
/**
* Exports given [SharedPreferences] to the file in given outputPath.
* It also creates the file.
*/
@Throws(Exception::class)
fun exportDatabase(preferences: SharedPreferences, file: StoredFileHelper) {
file.create()
ZipOutputStream(SharpOutputStream(file.stream).buffered()).use { outZip ->
// add the database
ZipHelper.addFileToZip(
outZip,
BackupFileLocator.FILE_NAME_DB,
fileLocator.db.path,
)
// add the legacy vulnerable serialized preferences (will be removed in the future)
ZipHelper.addFileToZip(
outZip,
BackupFileLocator.FILE_NAME_SERIALIZED_PREFS
) { byteOutput ->
ObjectOutputStream(byteOutput).use { output ->
output.writeObject(preferences.all)
output.flush()
}
}
// add the JSON preferences
ZipHelper.addFileToZip(
outZip,
BackupFileLocator.FILE_NAME_JSON_PREFS
) { byteOutput ->
JsonWriter
.indent("")
.on(byteOutput)
.`object`(preferences.all)
.done()
}
}
}
/**
* Tries to create database directory if it does not exist.
*
* @return Whether the directory exists afterwards.
*/
fun ensureDbDirectoryExists(): Boolean {
return fileLocator.dbDir.exists() || fileLocator.dbDir.mkdir()
}
/**
* Extracts the database from the given file to the app's database directory.
* The current app's database will be overwritten.
* @param file the .zip file to extract the database from
* @return true if the database was successfully extracted, false otherwise
*/
fun extractDb(file: StoredFileHelper): Boolean {
val success = ZipHelper.extractFileFromZip(
file,
BackupFileLocator.FILE_NAME_DB,
fileLocator.db.path,
)
if (success) {
fileLocator.dbJournal.delete()
fileLocator.dbWal.delete()
fileLocator.dbShm.delete()
}
return success
}
@Deprecated(
"Serializing preferences with Java's ObjectOutputStream is vulnerable to injections",
replaceWith = ReplaceWith("exportHasJsonPrefs")
)
fun exportHasSerializedPrefs(zipFile: StoredFileHelper): Boolean {
return ZipHelper.zipContainsFile(zipFile, BackupFileLocator.FILE_NAME_SERIALIZED_PREFS)
}
fun exportHasJsonPrefs(zipFile: StoredFileHelper): Boolean {
return ZipHelper.zipContainsFile(zipFile, BackupFileLocator.FILE_NAME_JSON_PREFS)
}
/**
* Remove all shared preferences from the app and load the preferences supplied to the manager.
*/
@Deprecated(
"Serializing preferences with Java's ObjectOutputStream is vulnerable to injections",
replaceWith = ReplaceWith("loadJsonPrefs")
)
@Throws(IOException::class, ClassNotFoundException::class)
fun loadSerializedPrefs(zipFile: StoredFileHelper, preferences: SharedPreferences) {
ZipHelper.extractFileFromZip(zipFile, BackupFileLocator.FILE_NAME_SERIALIZED_PREFS) {
PreferencesObjectInputStream(it).use { input ->
@Suppress("UNCHECKED_CAST")
val entries = input.readObject() as Map<String, *>
val editor = preferences.edit()
editor.clear()
for ((key, value) in entries) {
when (value) {
is Boolean -> editor.putBoolean(key, value)
is Float -> editor.putFloat(key, value)
is Int -> editor.putInt(key, value)
is Long -> editor.putLong(key, value)
is String -> editor.putString(key, value)
is Set<*> -> {
// There are currently only Sets with type String possible
@Suppress("UNCHECKED_CAST")
editor.putStringSet(key, value as Set<String>?)
}
}
}
if (!editor.commit()) {
throw IOException("Unable to commit loadSerializedPrefs")
}
}
}.let { fileExists ->
if (!fileExists) {
throw FileNotFoundException(BackupFileLocator.FILE_NAME_SERIALIZED_PREFS)
}
}
}
/**
* Remove all shared preferences from the app and load the preferences supplied to the manager.
*/
@Throws(IOException::class, JsonParserException::class)
fun loadJsonPrefs(zipFile: StoredFileHelper, preferences: SharedPreferences) {
ZipHelper.extractFileFromZip(zipFile, BackupFileLocator.FILE_NAME_JSON_PREFS) {
val jsonObject = JsonParser.`object`().from(it)
val editor = preferences.edit()
editor.clear()
for ((key, value) in jsonObject) {
when (value) {
is Boolean -> editor.putBoolean(key, value)
is Float -> editor.putFloat(key, value)
is Int -> editor.putInt(key, value)
is Long -> editor.putLong(key, value)
is String -> editor.putString(key, value)
is JsonArray -> {
editor.putStringSet(key, value.mapNotNull { e -> e as? String }.toSet())
}
}
}
if (!editor.commit()) {
throw IOException("Unable to commit loadJsonPrefs")
}
}.let { fileExists ->
if (!fileExists) {
throw FileNotFoundException(BackupFileLocator.FILE_NAME_JSON_PREFS)
}
}
}
}

View File

@@ -1,58 +0,0 @@
package org.schabi.newpipe.settings.export;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectStreamClass;
import java.util.Set;
/**
* An {@link ObjectInputStream} that only allows preferences-related types to be deserialized, to
* prevent injections. The only allowed types are: all primitive types, all boxed primitive types,
* null, strings. HashMap, HashSet and arrays of previously defined types are also allowed. Sources:
* <a href="https://wiki.sei.cmu.edu/confluence/display/java/SER00-J.+Enable+serialization+compatibility+during+class+evolution">
* cmu.edu
* </a>,
* <a href="https://cheatsheetseries.owasp.org/cheatsheets/Deserialization_Cheat_Sheet.html#harden-your-own-javaioobjectinputstream">
* OWASP cheatsheet
* </a>,
* <a href="https://commons.apache.org/proper/commons-io/apidocs/src-html/org/apache/commons/io/serialization/ValidatingObjectInputStream.html#line-118">
* Apache's {@code ValidatingObjectInputStream}
* </a>
*/
public class PreferencesObjectInputStream extends ObjectInputStream {
/**
* Primitive types, strings and other built-in types do not pass through resolveClass() but
* instead have a custom encoding; see
* <a href="https://docs.oracle.com/javase/6/docs/platform/serialization/spec/protocol.html#10152">
* official docs</a>.
*/
private static final Set<String> CLASS_WHITELIST = Set.of(
"java.lang.Boolean",
"java.lang.Byte",
"java.lang.Character",
"java.lang.Short",
"java.lang.Integer",
"java.lang.Long",
"java.lang.Float",
"java.lang.Double",
"java.lang.Void",
"java.util.HashMap",
"java.util.HashSet"
);
public PreferencesObjectInputStream(final InputStream in) throws IOException {
super(in);
}
@Override
protected Class<?> resolveClass(final ObjectStreamClass desc)
throws ClassNotFoundException, IOException {
if (CLASS_WHITELIST.contains(desc.getName())) {
return super.resolveClass(desc);
} else {
throw new ClassNotFoundException("Class not allowed: " + desc.getName());
}
}
}

View File

@@ -1,28 +1,24 @@
package org.schabi.newpipe.streams.io;
import static android.provider.DocumentsContract.Document.COLUMN_DISPLAY_NAME;
import static android.provider.DocumentsContract.Root.COLUMN_DOCUMENT_ID;
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.os.Build;
import android.os.storage.StorageManager;
import android.os.storage.StorageVolume;
import android.provider.DocumentsContract;
import android.system.Os;
import android.system.StructStatVfs;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.documentfile.provider.DocumentFile;
import org.schabi.newpipe.settings.NewPipeSettings;
import org.schabi.newpipe.util.FilePickerActivityHelper;
import java.io.FileDescriptor;
import java.io.IOException;
import java.net.URI;
import java.nio.file.Files;
@@ -31,9 +27,16 @@ import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static android.provider.DocumentsContract.Document.COLUMN_DISPLAY_NAME;
import static android.provider.DocumentsContract.Root.COLUMN_DOCUMENT_ID;
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
import us.shandian.giga.util.Utility;
public class StoredDirectoryHelper {
private static final String TAG = StoredDirectoryHelper.class.getSimpleName();
public static final int PERMISSION_FLAGS = Intent.FLAG_GRANT_READ_URI_PERMISSION
@@ -42,10 +45,6 @@ public class StoredDirectoryHelper {
private Path ioTree;
private DocumentFile docTree;
/**
* Context is `null` for non-SAF files, i.e. files that use `ioTree`.
*/
@Nullable
private Context context;
private final String tag;
@@ -177,43 +176,41 @@ public class StoredDirectoryHelper {
}
/**
* Get free memory of the storage partition this file belongs to (root of the directory).
* See <a href="https://stackoverflow.com/q/31171838">StackOverflow</a> and
* <a href="https://pubs.opengroup.org/onlinepubs/9699919799/functions/fstatvfs.html">
* {@code statvfs()} and {@code fstatvfs()} docs</a>
*
* @return amount of free memory in the volume of current directory (bytes), or {@link
* Long#MAX_VALUE} if an error occurred
* Get free memory of the storage partition (root of the directory).
* @return amount of free memory in the volume of current directory (bytes)
*/
public long getFreeStorageSpace() {
try {
final StructStatVfs stat;
@RequiresApi(api = Build.VERSION_CODES.N) // Necessary for `getStorageVolume()`
public long getFreeMemory() {
final Uri uri = getUri();
final StorageManager storageManager = (StorageManager) context.
getSystemService(Context.STORAGE_SERVICE);
final List<StorageVolume> volumes = storageManager.getStorageVolumes();
if (ioTree != null) {
// non-SAF file, use statvfs with the path directly (also, `context` would be null
// for non-SAF files, so we wouldn't be able to call `getContentResolver` anyway)
stat = Os.statvfs(ioTree.toString());
final String docId = DocumentsContract.getDocumentId(uri);
final String[] split = docId.split(":");
if (split.length > 0) {
final String volumeId = split[0];
} else {
// SAF file, we can't get a path directly, so obtain a file descriptor first
// and then use fstatvfs with the file descriptor
try (ParcelFileDescriptor parcelFileDescriptor =
context.getContentResolver().openFileDescriptor(getUri(), "r")) {
if (parcelFileDescriptor == null) {
return Long.MAX_VALUE;
for (final StorageVolume volume : volumes) {
// if the volume is an internal system volume
if (volume.isPrimary() && volumeId.equalsIgnoreCase("primary")) {
return Utility.getSystemFreeMemory();
}
// if the volume is a removable volume (normally an SD card)
if (volume.isRemovable() && !volume.isPrimary()) {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
try {
final String sdCardUUID = volume.getUuid();
return storageManager.getAllocatableBytes(UUID.fromString(sdCardUUID));
} catch (final Exception e) {
// do nothing
}
}
final FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor();
stat = Os.fstatvfs(fileDescriptor);
}
}
// this is the same formula used inside the FsStat class
return stat.f_bavail * stat.f_frsize;
} catch (final Throwable e) {
// ignore any error
Log.e(TAG, "Could not get free storage space", e);
return Long.MAX_VALUE;
}
return Long.MAX_VALUE;
}
/**

View File

@@ -245,7 +245,7 @@ public final class Localization {
* @return a formatted duration String or {@code 0:00} if the duration is zero.
*/
public static String getDurationString(final long duration) {
return getDurationString(duration, true, false);
return getDurationString(duration, true);
}
/**
@@ -254,11 +254,9 @@ public final class Localization {
* duration string.
* @param duration the duration in seconds
* @param isDurationComplete whether the given duration is complete or whether info is missing
* @param showDurationPrefix whether the duration-prefix shall be shown
* @return a formatted duration String or {@code 0:00} if the duration is zero.
*/
public static String getDurationString(final long duration, final boolean isDurationComplete,
final boolean showDurationPrefix) {
public static String getDurationString(final long duration, final boolean isDurationComplete) {
final String output;
final long days = duration / (24 * 60 * 60L); /* greater than a day */
@@ -276,9 +274,8 @@ public final class Localization {
} else {
output = String.format(Locale.US, "%d:%02d", minutes, seconds);
}
final String durationPrefix = showDurationPrefix ? "" : "";
final String durationPostfix = isDurationComplete ? "" : "+";
return durationPrefix + output + durationPostfix;
return output + durationPostfix;
}
/**

View File

@@ -1,21 +1,18 @@
package org.schabi.newpipe.util;
import org.schabi.newpipe.streams.io.SharpInputStream;
import org.schabi.newpipe.streams.io.StoredFileHelper;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
import org.schabi.newpipe.streams.io.StoredFileHelper;
/**
* Created by Christian Schabesberger on 28.01.18.
* Copyright 2018 Christian Schabesberger <chris.schabesberger@mailbox.org>
@@ -37,154 +34,73 @@ import java.util.zip.ZipOutputStream;
*/
public final class ZipHelper {
private ZipHelper() { }
private static final int BUFFER_SIZE = 2048;
@FunctionalInterface
public interface InputStreamConsumer {
void acceptStream(InputStream inputStream) throws IOException;
}
@FunctionalInterface
public interface OutputStreamConsumer {
void acceptStream(OutputStream outputStream) throws IOException;
}
private ZipHelper() { }
/**
* This function helps to create zip files. Caution this will overwrite the original file.
* This function helps to create zip files.
* Caution this will override the original file.
*
* @param outZip the ZipOutputStream where the data should be stored in
* @param nameInZip the path of the file inside the zip
* @param fileOnDisk the path of the file on the disk that should be added to zip
* @param outZip The ZipOutputStream where the data should be stored in
* @param file The path of the file that should be added to zip.
* @param name The path of the file inside the zip.
* @throws Exception
*/
public static void addFileToZip(final ZipOutputStream outZip,
final String nameInZip,
final String fileOnDisk) throws IOException {
try (FileInputStream fi = new FileInputStream(fileOnDisk)) {
addFileToZip(outZip, nameInZip, fi);
}
}
/**
* This function helps to create zip files. Caution this will overwrite the original file.
*
* @param outZip the ZipOutputStream where the data should be stored in
* @param nameInZip the path of the file inside the zip
* @param streamConsumer will be called with an output stream that will go to the output file
*/
public static void addFileToZip(final ZipOutputStream outZip,
final String nameInZip,
final OutputStreamConsumer streamConsumer) throws IOException {
final byte[] bytes;
try (ByteArrayOutputStream byteOutput = new ByteArrayOutputStream()) {
streamConsumer.acceptStream(byteOutput);
bytes = byteOutput.toByteArray();
}
try (ByteArrayInputStream byteInput = new ByteArrayInputStream(bytes)) {
ZipHelper.addFileToZip(outZip, nameInZip, byteInput);
}
}
/**
* This function helps to create zip files. Caution this will overwrite the original file.
*
* @param outZip the ZipOutputStream where the data should be stored in
* @param nameInZip the path of the file inside the zip
* @param inputStream the content to put inside the file
*/
public static void addFileToZip(final ZipOutputStream outZip,
final String nameInZip,
final InputStream inputStream) throws IOException {
public static void addFileToZip(final ZipOutputStream outZip, final String file,
final String name) throws Exception {
final byte[] data = new byte[BUFFER_SIZE];
try (BufferedInputStream bufferedInputStream =
new BufferedInputStream(inputStream, BUFFER_SIZE)) {
final ZipEntry entry = new ZipEntry(nameInZip);
try (FileInputStream fi = new FileInputStream(file);
BufferedInputStream inputStream = new BufferedInputStream(fi, BUFFER_SIZE)) {
final ZipEntry entry = new ZipEntry(name);
outZip.putNextEntry(entry);
int count;
while ((count = bufferedInputStream.read(data, 0, BUFFER_SIZE)) != -1) {
while ((count = inputStream.read(data, 0, BUFFER_SIZE)) != -1) {
outZip.write(data, 0, count);
}
}
}
/**
* This will extract data from ZipInputStream. Caution this will overwrite the original file.
*
* @param zipFile the zip file to extract from
* @param nameInZip the path of the file inside the zip
* @param fileOnDisk the path of the file on the disk where the data should be extracted to
* @return will return true if the file was found within the zip file
*/
public static boolean extractFileFromZip(final StoredFileHelper zipFile,
final String nameInZip,
final String fileOnDisk) throws IOException {
return extractFileFromZip(zipFile, nameInZip, input -> {
// delete old file first
final File oldFile = new File(fileOnDisk);
if (oldFile.exists()) {
if (!oldFile.delete()) {
throw new IOException("Could not delete " + fileOnDisk);
}
}
final byte[] data = new byte[BUFFER_SIZE];
try (FileOutputStream outFile = new FileOutputStream(fileOnDisk)) {
int count;
while ((count = input.read(data)) != -1) {
outFile.write(data, 0, count);
}
}
});
}
/**
* This will extract data from ZipInputStream.
* Caution this will override the original file.
*
* @param zipFile the zip file to extract from
* @param nameInZip the path of the file inside the zip
* @param streamConsumer will be called with the input stream from the file inside the zip
* @param zipFile The zip file
* @param file The path of the file on the disk where the data should be extracted to.
* @param name The path of the file inside the zip.
* @return will return true if the file was found within the zip file
* @throws Exception
*/
public static boolean extractFileFromZip(final StoredFileHelper zipFile,
final String nameInZip,
final InputStreamConsumer streamConsumer)
throws IOException {
try (ZipInputStream inZip = new ZipInputStream(new BufferedInputStream(
new SharpInputStream(zipFile.getStream())))) {
ZipEntry ze;
while ((ze = inZip.getNextEntry()) != null) {
if (ze.getName().equals(nameInZip)) {
streamConsumer.acceptStream(inZip);
return true;
}
}
return false;
}
}
/**
* @param zipFile the zip file
* @param fileInZip the filename to check
* @return whether the provided filename is in the zip; only the first level is checked
*/
public static boolean zipContainsFile(final StoredFileHelper zipFile, final String fileInZip)
throws Exception {
public static boolean extractFileFromZip(final StoredFileHelper zipFile, final String file,
final String name) throws Exception {
try (ZipInputStream inZip = new ZipInputStream(new BufferedInputStream(
new SharpInputStream(zipFile.getStream())))) {
final byte[] data = new byte[BUFFER_SIZE];
boolean found = false;
ZipEntry ze;
while ((ze = inZip.getNextEntry()) != null) {
if (ze.getName().equals(fileInZip)) {
return true;
if (ze.getName().equals(name)) {
found = true;
// delete old file first
final File oldFile = new File(file);
if (oldFile.exists()) {
if (!oldFile.delete()) {
throw new Exception("Could not delete " + file);
}
}
try (FileOutputStream outFile = new FileOutputStream(file)) {
int count = 0;
while ((count = inZip.read(data)) != -1) {
outFile.write(data, 0, count);
}
}
inZip.closeEntry();
}
}
return false;
return found;
}
}

View File

@@ -1,15 +0,0 @@
package org.schabi.newpipe.util.debounce;
import org.schabi.newpipe.error.ErrorInfo;
public interface DebounceSavable {
/**
* Execute operations to save the data. <br>
* Must set {@link DebounceSaver#setIsModified(boolean)} false in this method manually
* after the data has been saved.
*/
void saveImmediate();
void showError(ErrorInfo errorInfo);
}

View File

@@ -1,81 +0,0 @@
package org.schabi.newpipe.util.debounce;
import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.UserAction;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.subjects.PublishSubject;
public class DebounceSaver {
private final long saveDebounceMillis;
private final PublishSubject<Long> debouncedSaveSignal;
private final DebounceSavable debounceSavable;
// Has the object been modified
private final AtomicBoolean isModified;
// Default 10 seconds
private static final long DEFAULT_SAVE_DEBOUNCE_MILLIS = 10000;
/**
* Creates a new {@code DebounceSaver}.
*
* @param saveDebounceMillis Save the object milliseconds later after the last change
* occurred.
* @param debounceSavable The object containing data to be saved.
*/
public DebounceSaver(final long saveDebounceMillis, final DebounceSavable debounceSavable) {
this.saveDebounceMillis = saveDebounceMillis;
debouncedSaveSignal = PublishSubject.create();
this.debounceSavable = debounceSavable;
this.isModified = new AtomicBoolean();
}
/**
* Creates a new {@code DebounceSaver}. Save the object 10 seconds later after the last change
* occurred.
*
* @param debounceSavable The object containing data to be saved.
*/
public DebounceSaver(final DebounceSavable debounceSavable) {
this(DEFAULT_SAVE_DEBOUNCE_MILLIS, debounceSavable);
}
public boolean getIsModified() {
return isModified.get();
}
public void setNoChangesToSave() {
isModified.set(false);
}
public PublishSubject<Long> getDebouncedSaveSignal() {
return debouncedSaveSignal;
}
public Disposable getDebouncedSaver() {
return debouncedSaveSignal
.debounce(saveDebounceMillis, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(ignored -> debounceSavable.saveImmediate(), throwable ->
debounceSavable.showError(new ErrorInfo(throwable,
UserAction.SOMETHING_ELSE, "Debounced saver")));
}
public void setHasChangesToSave() {
if (isModified == null || debouncedSaveSignal == null) {
return;
}
isModified.set(true);
debouncedSaveSignal.onNext(System.currentTimeMillis());
}
}

View File

@@ -40,6 +40,20 @@ public class Utility {
UNKNOWN
}
/**
* Get amount of free system's memory.
* @return free memory (bytes)
*/
public static long getSystemFreeMemory() {
try {
final StatFs statFs = new StatFs(Environment.getExternalStorageDirectory().getPath());
return statFs.getAvailableBlocksLong() * statFs.getBlockSizeLong();
} catch (final Exception e) {
// do nothing
}
return -1;
}
public static String formatBytes(long bytes) {
Locale locale = Locale.getDefault();
if (bytes < 1024) {

View File

@@ -1,84 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/itemRoot"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:padding="@dimen/video_item_search_padding">
<ImageView
android:id="@+id/itemThumbnailView"
android:layout_width="90dp"
android:layout_height="50dp"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:layout_marginRight="@dimen/video_item_search_image_right_margin"
android:scaleType="centerCrop"
android:src="@drawable/placeholder_thumbnail_playlist"
tools:ignore="RtlHardcoded" />
<org.schabi.newpipe.views.NewPipeTextView
android:id="@+id/itemStreamCountView"
android:layout_width="45dp"
android:layout_height="match_parent"
android:layout_alignTop="@id/itemThumbnailView"
android:layout_alignRight="@id/itemThumbnailView"
android:layout_alignBottom="@id/itemThumbnailView"
android:background="@color/playlist_stream_count_background_color"
android:gravity="center"
android:paddingTop="4dp"
android:paddingBottom="6dp"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="@color/duration_text_color"
android:textSize="@dimen/video_item_search_duration_text_size"
android:textStyle="bold"
app:drawableTint="@color/duration_text_color"
app:drawableTopCompat="@drawable/ic_playlist_play"
tools:ignore="RtlHardcoded"
tools:text="3141" />
<ImageView
android:id="@+id/itemHandle"
android:layout_width="wrap_content"
android:layout_height="55dp"
android:layout_alignParentRight="true"
android:layout_gravity="center_vertical"
android:contentDescription="@string/detail_drag_description"
android:paddingLeft="@dimen/video_item_search_image_right_margin"
android:scaleType="center"
app:srcCompat="@drawable/ic_drag_handle"
tools:ignore="RtlHardcoded,RtlSymmetry" />
<org.schabi.newpipe.views.NewPipeTextView
android:id="@+id/itemTitleView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_toStartOf="@id/itemHandle"
android:layout_toLeftOf="@id/itemHandle"
android:layout_toRightOf="@+id/itemThumbnailView"
android:ellipsize="end"
android:maxLines="2"
android:textAppearance="?android:attr/textAppearanceLarge"
android:textSize="@dimen/video_item_search_title_text_size"
tools:ignore="RtlHardcoded"
tools:text="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc tristique vitae sem vitae blanditLorem ipsumLorem ipsumLorem ipsumLorem ipsumLorem ipsumLorem ipsumLorem ipsum" />
<org.schabi.newpipe.views.NewPipeTextView
android:id="@+id/itemUploaderView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/itemTitleView"
android:layout_toRightOf="@+id/itemThumbnailView"
android:lines="1"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textSize="@dimen/video_item_search_uploader_text_size"
tools:ignore="RtlHardcoded"
tools:text="Uploader" />
</RelativeLayout>

View File

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

View File

@@ -869,15 +869,4 @@
<string name="show_more">عرض المزيد</string>
<string name="show_less">عرض أقل</string>
<string name="notification_actions_summary_android13">قم بتحرير كل إجراء إعلام أدناه من خلال النقر عليه. يتم تعيين الإجراءات الثلاثة الأولى (تشغيل/إيقاف مؤقت، السابق والتالي) بواسطة النظام ولا يمكن تخصيصها.</string>
<string name="error_insufficient_storage">لا توجد مساحة خالية كافية على الجهاز</string>
<string name="reset_settings_title">اعادة ضبط الإعداداتِ</string>
<string name="settings_category_backup_restore_title">النسخ الاحتياطيُّ والاستعادة</string>
<string name="reset_settings_summary">أعيدوا جميع الإعدادات إلى قيمهم الافتراضية</string>
<string name="reset_all_settings">ستؤدي إعادة ضبط جميع الإعدادات إلى تجاهل جميع إعداداتك المفضلة وإعادة تشغيل التطبيق.
\n
\nهل انت متأكد انك تريد المتابعة؟</string>
<string name="yes">نعم</string>
<string name="auto_update_check_description">يمكن لـ NewPipe البحث تلقائيًا عن الإصدارات الجديدة من وقت لآخر وإعلامك بمجرد توفرها.
\nهل تريد تمكين هذا؟</string>
<string name="no">لا</string>
</resources>

View File

@@ -839,5 +839,4 @@
<string name="show_more">Паказаць больш</string>
<string name="show_less">Паказаць менш</string>
<string name="notification_actions_summary_android13">Адрэдагуйце кожнае дзеянне апавяшчэння, націснуўшы на яго. Першыя тры дзеянні (прайграванне/паўза, папярэдняе і наступнае) задаюцца сістэмай і не могуць быць зменены.</string>
<string name="error_insufficient_storage">Недастаткова вольнага месца на прыладзе</string>
</resources>

View File

@@ -827,15 +827,4 @@
<string name="show_more">Zobrazit více</string>
<string name="notification_actions_summary_android13">Upravte každou akci oznámení níže poklepáním. První tři akce (přehrání/pozastavení, předchozí a další) jsou nastaveny systémem a nemohou být přizpůsobeny.</string>
<string name="show_less">Zobrazit méně</string>
<string name="error_insufficient_storage">Nedostatek volného místa v zařízení</string>
<string name="settings_category_backup_restore_title">Záloha a obnovení</string>
<string name="reset_settings_title">Obnovit nastavení</string>
<string name="reset_settings_summary">Obnovení všech nastavení na výchozí hodnoty</string>
<string name="reset_all_settings">Obnovením nastavení se zruší všechna preferovaná nastavení a aplikace se restartuje.
\n
\nJste si jisti, že chcete pokračovat?</string>
<string name="yes">Ano</string>
<string name="no">Ne</string>
<string name="auto_update_check_description">NewPipe může čas od času automaticky kontrolovat nové verze a upozornit vás na jejich dostupnost.
\nChcete tuto funkci povolit?</string>
</resources>

View File

@@ -813,15 +813,4 @@
<string name="show_more">Mehr zeigen</string>
<string name="show_less">Weniger zeigen</string>
<string name="notification_actions_summary_android13">Bearbeite jede Benachrichtigungsaktion unten, indem du auf sie tippst. Die ersten drei Aktionen (Abspielen/Pause, Zurück und Weiter) sind vom System vorgegeben und können nicht angepasst werden.</string>
<string name="error_insufficient_storage">Nicht genug freier Speicher auf dem Gerät</string>
<string name="reset_settings_title">Einstellungen zurücksetzen</string>
<string name="reset_settings_summary">Setzt alle Einstellungen auf ihre Standardwerte zurück</string>
<string name="yes">Ja</string>
<string name="no">Nein</string>
<string name="settings_category_backup_restore_title">Sichern und Wiederherstellen</string>
<string name="auto_update_check_description">NewPipe kann von Zeit zu Zeit automatisch nach neuen Versionen suchen und dich benachrichtigen, sobald sie verfügbar sind.
\nMöchtest du dies aktivieren?</string>
<string name="reset_all_settings">Wenn du alle Einstellungen zurücksetzt, werden alle deine bevorzugten Einstellungen verworfen und die App wird neu gestartet.
\n
\nMöchtest du wirklich fortfahren?</string>
</resources>

View File

@@ -813,15 +813,4 @@
<string name="show_more">Εμφάνιση περισσοτέρων</string>
<string name="show_less">Εμφάνιση λιγότερων</string>
<string name="notification_actions_summary_android13">Επεξεργαστείτε κάθε ενέργεια ειδοποίησης παρακάτω πατώντας σε αυτήν. Οι τρεις πρώτες ενέργειες (αναπαραγωγή/παύση, προηγούμενηο και επόμενο) ορίζονται από το σύστημα και δεν μπορούν να τροποποιηθούν.</string>
<string name="error_insufficient_storage">Δεν υπάρχει αρκετός ελεύθερος χώρος στη συσκευή</string>
<string name="no">Όχι</string>
<string name="yes">Ναι</string>
<string name="settings_category_backup_restore_title">Αντίγραφο ασφαλείας και επαναφορά</string>
<string name="auto_update_check_description">Το NewPipe μπορεί να ελέγχει αυτόματα για νέες εκδόσεις και να σας ειδοποιεί μόλις είναι διαθέσιμες.
\nΘέλετε να το ενεργοποιήσετε;</string>
<string name="reset_settings_title">Επαναφορά ρυθμίσεων</string>
<string name="reset_settings_summary">Επαναφορά όλων των ρυθμίσεων στις αρχικές τιμές τους</string>
<string name="reset_all_settings">Η επαναφορά όλων των ρυθμίσεων θα απορρίψει όλες τις τροποποιημένες ρυθμίσεις σας και θα επανεκκινήσει την εφαρμογή.
\n
\nΕίστε βέβαιοι ότι θέλετε να συνεχίσετε;</string>
</resources>

View File

@@ -782,7 +782,7 @@
<string name="metadata_subscribers">Suscriptores</string>
<string name="show_channel_tabs_summary">Qué pestañas se muestran en las páginas de los canales</string>
<string name="show_channel_tabs">Pestañas del canal</string>
<string name="channel_tab_shorts">Shorts</string>
<string name="channel_tab_shorts">Cortos</string>
<string name="loading_metadata_title">Cargando los metadatos…</string>
<string name="feed_fetch_channel_tabs">Recuperar las fichas del canal</string>
<string name="channel_tab_about">Acerca de</string>
@@ -830,15 +830,4 @@
<string name="show_more">Ver más</string>
<string name="show_less">Mostrar menos</string>
<string name="notification_actions_summary_android13">Edite cada acción de notificación pulsando sobre ella. Las tres primeras acciones (reproducir/pausa, anterior y siguiente) las establece el sistema y no se pueden personalizar.</string>
<string name="error_insufficient_storage">No hay suficiente espacio libre en el dispositivo</string>
<string name="settings_category_backup_restore_title">Copia de seguridad y restaurar</string>
<string name="reset_settings_title">Reiniciar ajustes</string>
<string name="reset_settings_summary">Restablecer todos los ajustes a sus valores predeterminados</string>
<string name="reset_all_settings">Restablecer todos los ajustes descartará todos sus ajustes preferidos y reiniciará la aplicación.
\n
\n¿Estas seguro que deseas continuar?</string>
<string name="yes"></string>
<string name="no">No</string>
<string name="auto_update_check_description">NewPipe puede buscar automáticamente nuevas versiones de vez en cuando y notificarle cuando estén disponibles.
\n¿Quieres habilitar esto?</string>
</resources>

View File

@@ -813,15 +813,4 @@
</plurals>
<string name="show_less">Näita vähem</string>
<string name="notification_actions_summary_android13">Muuda iga teavituse tegevust sellel toksates. Kolm esimest tegevust (esita/peata esitus, eelmine video, järgmine video) on süsteemsed ja neid ei saa muuta.</string>
<string name="settings_category_backup_restore_title">Varundus ja taastamine</string>
<string name="auto_update_check_description">NewPipe võib aeg-ajalt automaatselt kontrollida uute versioonide olemasolu ning sind vastavalt teavitada.
\nKas sa soovid sellist võimalust kasuutada?</string>
<string name="reset_settings_title">Lähtesta seadistused</string>
<string name="reset_settings_summary">Lähtesta kõik seadistused nende vaikimisi väärtusteks</string>
<string name="error_insufficient_storage">Seadmes pole enam piisavalt vaba ruumi</string>
<string name="reset_all_settings">Kui lähtestad kõik seadistused, siis kõik sinu muudetud seadistused asendatakse vaikimisi väärtustega ja rakendus käivitub uuesti.
\n
\nKas sa soovid jätkata?</string>
<string name="yes">Jah</string>
<string name="no">Ei</string>
</resources>

View File

@@ -829,15 +829,4 @@
<string name="notification_actions_summary_android13">Modifiez chaque action de notification ci-dessous en appuyant dessus. Les trois premières actions (lire/pause, précédent, suivant) sont définies par le système et ne peuvent pas être personnalisées.</string>
<string name="show_more">Afficher plus</string>
<string name="show_less">Afficher moins</string>
<string name="reset_settings_summary">Réinitialiser tous les paramètres à leurs valeurs par défaut</string>
<string name="no">Non</string>
<string name="reset_all_settings">La réinitialisation de tous les paramètres va supprimer toutes vos préférences de paramètres et redémarrer l\'application.
\n
\nÊtes-vous sûr de vouloir poursuivre?</string>
<string name="settings_category_backup_restore_title">Sauvegarde et restauration</string>
<string name="yes">Oui</string>
<string name="auto_update_check_description">NewPipe peut automatiquement vérifier la disponibilité de nouvelles versions de temps en temps et vous notifier lorsqu\'elles sont disponibles.
\nVoulez-vous activer cette vérification?</string>
<string name="reset_settings_title">Réinitialiser les paramètres</string>
<string name="error_insufficient_storage">Pas assez d\'espace disponible sur l\'appareil</string>
</resources>

View File

@@ -1,20 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="no_player_found">aucun streamer trouvé . Installez VLC?</string>
<string name="no">non</string>
<string name="open_in_browser">ouvrir dans le browser</string>
<string name="open_in_popup_mode">ouvrir dans le popup mode</string>
<string name="open_with">ouvrir avec</string>
<string name="share">partagez</string>
<string name="controls_download_desc">installer le fichier stream</string>
<string name="search">chercher</string>
<string name="settings">parameters</string>
<string name="download">installer</string>
<string name="install">Installer</string>
<string name="mark_as_watched">marquer comme vu</string>
<string name="upload_date_text">"publié le %1$s"</string>
<string name="no_player_found_toast">aucun joueur de stream n\'est trouvé ( vous pouvez installez VLC pour jouer)</string>
<string name="cancel">Annuler</string>
<string name="ok">OK</string>
<string name="yes">Oui</string>
</resources>

View File

@@ -813,15 +813,4 @@
<string name="show_more">और दिखाओ</string>
<string name="notification_actions_summary_android13">नीचे दी गई प्रत्येक अधिसूचना कार्रवाई पर टैप करके उसे संपादित करें। पहली तीन क्रियाएँ (चलाएँ/रोकें, पिछली और अगली) सिस्टम द्वारा निर्धारित की जाती हैं और इन्हें अनुकूलित नहीं किया जा सकता है।</string>
<string name="show_less">कम दिखाएं</string>
<string name="auto_update_check_description">न्यूपाइप समय-समय पर स्वचालित रूप से नए संस्करणों की जांच कर सकती है और उपलब्ध होने पर आपको सूचित कर सकती है।
\nक्या आप इसे सक्षम करना चाहते हैं?</string>
<string name="reset_settings_title">सेटिंग्स रीसेट करें</string>
<string name="reset_settings_summary">सभी सेटिंग्स को उनके डिफ़ॉल्ट मानों पर रीसेट करें</string>
<string name="reset_all_settings">सभी सेटिंग्स को रीसेट करने से आपकी सभी पसंदीदा सेटिंग्स खारिज हो जाएंगी और ऐप पुनः प्रारंभ हो जाएगा।
\n
\nक्या आप सुनिश्चित रूप से आगे बढ़ना चाहते हैं?</string>
<string name="yes">हाँ</string>
<string name="no">नहीं</string>
<string name="error_insufficient_storage">डिवाइस पर पर्याप्त खाली स्थान नहीं है</string>
<string name="settings_category_backup_restore_title">बैकअप और रिस्टोर</string>
</resources>

View File

@@ -574,7 +574,7 @@
\nYouTube je primjer usluge koja nudi ovaj brzi način sa svojim RSS feedom.
\n
\nDakle, izbor se svodi na ono što više voliš: brzinu ili precizne informacije.</string>
<string name="msg_calculating_hash">Izračunavanje šifre</string>
<string name="msg_calculating_hash">Izračunavanje šifriranja</string>
<string name="hash_channel_name">Obavijest šifriranja videa</string>
<string name="hash_channel_description">Obavijesti o napretku šifriranja videa</string>
<string name="recent">Nedavni</string>
@@ -733,7 +733,7 @@
<string name="feed_show_upcoming">Najava</string>
<string name="sort">Razvrstaj</string>
<string name="use_exoplayer_decoder_fallback_title">Koristi razervnu funkciju ExoPlayer dekodera</string>
<string name="ignore_hardware_media_buttons_title">Ignoriraj događaje hardverskih medijskih gumba</string>
<string name="ignore_hardware_media_buttons_title">Ignoriranje hardverskih medijskih gumba</string>
<string name="ignore_hardware_media_buttons_summary">Korisno, na primjer, ako koristite slušalice s pokvarenim fizičkim gumbima</string>
<string name="prefer_descriptive_audio_summary">Odaberite zvučni zapis s opisima za slabovidne osobe ako je dostupan</string>
<string name="prefer_original_audio_title">Preferiraj originalni zvuk</string>
@@ -821,16 +821,4 @@
<item quantity="other">%s odgovora</item>
</plurals>
<string name="disable_media_tunneling_automatic_info">Tuneliranje medija je standardno deaktivirano na tvom uređaju jer je poznato da model tvog uređaja to ne podržava.</string>
<string name="yes">Da</string>
<string name="no">Ne</string>
<string name="use_exoplayer_decoder_fallback_summary">Aktiviraj ovu opciju ako imaš problema s inicijaliziranjem dekodera, što vraća dekodere nižeg prioriteta ako inicijaliziranje primarnih dekodera ne uspije. To može rezultirati lošijim performansama reprodukcije u odnosu na korištenje primarnih dekodera</string>
<string name="error_insufficient_storage">Nedovoljno memorije na uređaju</string>
<string name="settings_category_backup_restore_title">Spremanje sigurnosne kopije i obnavljanje</string>
<string name="auto_update_check_description">NewPipe može automatski tražiti nove verzije i obavijestiti te.
\nŽeliš li aktivirati tu mogućnost?</string>
<string name="reset_settings_title">Obnovi postavke</string>
<string name="reset_settings_summary">Obnovi sve postavke na zadane vrijednosti</string>
<string name="reset_all_settings">Obnavljanje svih postavki odbacit će sve tvoje postavljene postavke i aplikacija će se ponovo pokrenuti.
\n
\nStvarno želiš nastaviti?</string>
</resources>

View File

@@ -813,15 +813,4 @@
<string name="channel_tab_tracks">Dalok</string>
<string name="channel_tab_shorts">Rövidek</string>
<string name="channel_tab_livestreams">Élő</string>
<string name="error_insufficient_storage">Nincs elég szabad hely az eszközön</string>
<string name="yes">Igen</string>
<string name="no">Nem</string>
<string name="settings_category_backup_restore_title">Biztonsági mentés és visszaállítás</string>
<string name="auto_update_check_description">A NewPipe időről időre automatikusan ellenőrzi az új verziókat, és értesít, amint azok elérhetővé válnak.
\nSzeretné engedélyezni ezt?</string>
<string name="reset_settings_title">Beállítások alaphelyzetbe állítása</string>
<string name="reset_settings_summary">Minden beállítás visszaállítása alapértelmezett értékre</string>
<string name="reset_all_settings">Az összes beállítás visszaállítása elveti az összes preferált beállítást, és újraindítja az alkalmazást.
\n
\nBiztosan folytatja?</string>
</resources>

View File

@@ -799,15 +799,4 @@
<plurals name="replies">
<item quantity="other">%s balasan</item>
</plurals>
<string name="error_insufficient_storage">Tidak ada ruang yang cukup pada perangkat</string>
<string name="yes">Ya</string>
<string name="no">Tidak</string>
<string name="reset_settings_title">Atur ulang pengaturan</string>
<string name="reset_settings_summary">Atur ulang pengaturan ke nilai bawaan</string>
<string name="reset_all_settings">Mengatur ulang semua pengaturan akan mengabaikan pengaturan Anda yang disukai dan memulai ulang aplikasi.
\n
\nApakah Anda yakin ingin melanjutkan?</string>
<string name="auto_update_check_description">NewPipe dapat memeriksa versi baru secara berkala dan memberi tahu Anda ketika ada yang baru.
\nApakah Anda ingin mengaktifkan ini?</string>
<string name="settings_category_backup_restore_title">Cadangkan dan pulihkan</string>
</resources>

View File

@@ -827,15 +827,4 @@
<string name="show_more">Mostra altro</string>
<string name="notification_actions_summary_android13">Le azioni dei pulsanti della notifica possono essere modificate qua sotto. Le prime tre (riproduci/pausa, precedente e successivo) sono impostate dal sistema e non possono essere cambiate.</string>
<string name="show_less">Mostra meno</string>
<string name="error_insufficient_storage">Non abbastanza spazio libero sul dispositivo</string>
<string name="settings_category_backup_restore_title">Backup e ripristino</string>
<string name="reset_settings_title">Azzera le impostazioni</string>
<string name="reset_all_settings">L\'azzeramento di tutte le impostazioni eliminerà tutte le proprie impostazioni e riavvierà l\'app.
\n
\nSei sicuro di voler procedere?</string>
<string name="reset_settings_summary">Azzera tutte le impostazioni ai loro valori predefiniti</string>
<string name="yes"></string>
<string name="no">No</string>
<string name="auto_update_check_description">NewPipe può cercare automaticamente nuove versioni di tanto in tanto e avvisarti quando sono disponibili.
\nVuoi attivarlo?</string>
</resources>

View File

@@ -799,15 +799,4 @@
<string name="show_more">もっと見る</string>
<string name="show_less">表示を少なくする</string>
<string name="notification_actions_summary_android13">以下の通知アクションをタップして編集します。 最初の3つのアクション (再生/一時停止、前へ、次へ)はシステムによって設定されており、カスタマイズすることはできません。</string>
<string name="error_insufficient_storage">デバイスの空き容量が不足しています</string>
<string name="settings_category_backup_restore_title">バックアップと復元</string>
<string name="yes">はい</string>
<string name="no">いいえ</string>
<string name="auto_update_check_description">NewPipe は定期的に新しいバージョンを自動的にチェックし、更新可能になると通知します。
\n有効にしますか</string>
<string name="reset_settings_title">設定をリセット</string>
<string name="reset_settings_summary">全ての設定をデフォルト状態にリセットします</string>
<string name="reset_all_settings">全ての設定をリセットすると、優先設定が全て破棄され、アプリが再起動します。
\n
\n続行しますか</string>
</resources>

View File

@@ -40,6 +40,4 @@
<string name="tab_choose">ಟ್ಯಾಬ್ ಆಯ್ಕೆಮಾಡಿ</string>
<string name="controls_background_title">ಹಿನ್ನೆಲೆ</string>
<string name="unsubscribe">ಅನ್‌ಸಬ್‌ಸ್ಕ್ರೈಬ್ ಮಾಡಿ</string>
<string name="yes">ಹೌದು</string>
<string name="no">ಇಲ್ಲ</string>
</resources>

View File

@@ -505,8 +505,8 @@
<string name="show_description_title">설명 표시하기</string>
<string name="clear_queue_confirmation_summary">한 플레이어에서 다른 플레이어로 전환하면 대기열이 대체될 수 있습니다</string>
<string name="night_theme_title">어두운 테마</string>
<string name="notification_actions_at_most_three">축소된 알림에서 최대 3개까지 표시될 항목을 고를 수 있습니다!</string>
<string name="notification_actions_summary">아래의 각 알림 작업을 눌러 편집하세요 오른쪽에 있는 확인란을 사용하여 압축 알림에 표시할 항목을 최대 3개까지 선택</string>
<string name="notification_actions_at_most_three">최대 3개까지 축소 알림에 표시될 항목을 고를 수 있습니다!</string>
<string name="notification_actions_summary">아래의 각 알림 작업을 눌러 편집하세요. 오른쪽에 있는 확인란을 사용하여 압축 알림에 표시할 항목을 최대 3개까지 선택</string>
<string name="youtube_restricted_mode_enabled_title">YouTube의 \'제한 모드\' 켜기</string>
<string name="hash_channel_description">비디오 해싱 진행 알림</string>
<string name="streams_notification_channel_name">새로운 스트림</string>
@@ -793,5 +793,4 @@
<string name="channel_tab_channels">채널</string>
<string name="previous_stream">이전 스트림</string>
<string name="channel_tab_livestreams">실시간</string>
<string name="notification_actions_summary_android13">아래의 각 알림 작업을 탭하여 편집하세요. 처음 세 가지 작업(재생/일시 중지, 이전 및 다음)은 시스템에 의해 설정되며 사용자 정의할 수 없습니다.</string>
</resources>

View File

@@ -205,8 +205,8 @@
<string name="popup_player">Pop-upspeler</string>
<string name="preferred_player_fetcher_notification_title">Bezig met ophalen van informatie…</string>
<string name="preferred_player_fetcher_notification_message">Bezig met laden van gevraagde inhoud</string>
<string name="import_data_title">Data­base importeren</string>
<string name="export_data_title">Data­base exporteren</string>
<string name="import_data_title">Databank importeren</string>
<string name="export_data_title">Databank exporteren</string>
<string name="import_data_summary">Dit overschrijft je huidige geschiedenis, abonnementen, afspeellijsten en instellingen</string>
<string name="export_data_summary">Exporteer geschiedenis, abonnementen, afspeellijsten en instellingen</string>
<string name="export_complete_toast">Geëxporteerd</string>
@@ -637,7 +637,7 @@
<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>
<string name="mark_as_watched">Markeren als bekeken</string>
<string name="mark_as_watched">Markeer als bekeken</string>
<plurals name="deleted_downloads_toast">
<item quantity="one">%1$s download verwijderd</item>
<item quantity="other">%1$s downloads verwijderd</item>
@@ -813,15 +813,4 @@
<string name="show_more">Meer tonen</string>
<string name="show_less">Minder tonen</string>
<string name="notification_actions_summary_android13">Bewerk elke meldings­actie hieronder door erop te tikken. De eerste drie acties (afspelen/pauzeren, vorige en volgende) zijn ingesteld door het systeem en kunnen niet worden aangepast.</string>
<string name="error_insufficient_storage">Onvoldoende vrije ruimte op het apparaat</string>
<string name="yes">Ja</string>
<string name="reset_settings_summary">Herstelt alle instellingen naar hun standaard­waarde</string>
<string name="reset_all_settings">Als u alle instellingen reset worden al uw voorkeurs­instellingen verwijderd en wordt de app opnieuw gestart.
\n
\nWeet u zeker dat u verder wilt gaan?</string>
<string name="no">Nee</string>
<string name="settings_category_backup_restore_title">Back-up en herstel</string>
<string name="auto_update_check_description">NewPipe kan van tijd tot tijd auto­matisch controleren op nieuwe versies en u op de hoogte stellen zodra deze beschik­baar zijn.
\nWilt u dit inschakelen?</string>
<string name="reset_settings_title">Instellingen resetten</string>
</resources>

View File

@@ -813,15 +813,4 @@
<item quantity="other">%s ଉତ୍ତରଗୁଡ଼ିକ</item>
</plurals>
<string name="show_less">କମ୍ ଦର୍ଶାନ୍ତୁ</string>
<string name="error_insufficient_storage">ଉପକରଣରେ ପର୍ଯ୍ୟାପ୍ତ ଖାଲି ସ୍ଥାନ ନାହିଁ</string>
<string name="settings_category_backup_restore_title">ବ୍ୟାକଅପ୍ ଏବଂ ପୁନରୁଦ୍ଧାର କରନ୍ତୁ</string>
<string name="yes">ହଁ</string>
<string name="no">ନା</string>
<string name="auto_update_check_description">NewPipe ସ୍ୱୟଂଚାଳିତ ଭାବରେ ସମୟ ସମୟରେ ନୂତନ ସଂସ୍କରଣଗୁଡିକ ଯାଞ୍ଚ କରିପାରିବ ଏବଂ ଥରେ ଉପଲବ୍ଧ ହେବା ପରେ ଆପଣଙ୍କୁ ସୂଚିତ କରିପାରିବ ।
\nଆପଣ ଏହାକୁ ସକ୍ଷମ କରିବାକୁ ଚାହୁଁଛନ୍ତି କି?</string>
<string name="reset_settings_title">ସେଟିଂସମୂହ ପୁନଃସେଟ୍ କରନ୍ତୁ</string>
<string name="reset_settings_summary">ସମସ୍ତ ସେଟିଂସମୂହକୁ ସେମାନଙ୍କର ଡିଫଲ୍ଟ ମୂଲ୍ୟରେ ପୁନଃସେଟ୍ କରନ୍ତୁ</string>
<string name="reset_all_settings">ସମସ୍ତ ସେଟିଂସମୂହକୁ ପୁନଃ ସେଟ୍ କରିବା ଦ୍ବାରା ଆପଣଙ୍କର ସମସ୍ତ ପସନ୍ଦିତ ସେଟିଂସମୂହ ପରିତ୍ୟାଗ ହେବ ଏବଂ ଆପ୍ ପୁନଃ ସେଟ୍ଆରମ୍ଭ ହେବ ।
\n
\nଆପଣ ଆଗକୁ ବଢିବାକୁ ଚାହୁଁଛନ୍ତି କି?</string>
</resources>

View File

@@ -813,15 +813,4 @@
<string name="show_more">ਹੋਰ ਵਿਖਾਓ</string>
<string name="notification_actions_summary_android13">ਇਸ \'ਤੇ ਟੈਪ ਕਰਕੇ ਹੇਠਾਂ ਹਰੇਕ ਸੂਚਨਾ ਕਾਰਵਾਈ ਨੂੰ ਸੰਪਾਦਿਤ ਕਰੋ। ਪਹਿਲੀਆਂ ਤਿੰਨ ਕਾਰਵਾਈਆਂ (ਪਲੇ/ਪੌਜ਼, ਪਿਛਲਾ ਅਤੇ ਅਗਲਾ) ਸਿਸਟਮ ਦੁਆਰਾ ਸੈੱਟ ਕੀਤੀਆਂ ਗਈਆਂ ਹਨ ਅਤੇ ਉਹਨਾਂ ਨੂੰ ਅਨੁਕੂਲਿਤ ਨਹੀਂ ਕੀਤਾ ਜਾ ਸਕਦਾ ਹੈ।</string>
<string name="show_less">ਘੱਟ ਦਿਖਾਓ</string>
<string name="yes">ਹਾਂ</string>
<string name="auto_update_check_description">ਨਿਊਪਾਈਪ ਸਮੇਂ-ਸਮੇਂ \'ਤੇ ਨਵੇਂ ਸੰਸਕਰਣਾਂ ਦੀ ਸਵੈਚਲਿਤ ਤੌਰ \'ਤੇ ਜਾਂਚ ਕਰ ਸਕਦੀ ਹੈ ਅਤੇ ਇੱਕ ਵਾਰ ਉਪਲਬਧ ਹੋਣ \'ਤੇ ਤੁਹਾਨੂੰ ਸੂਚਿਤ ਕਰ ਸਕਦੀ ਹੈ।
\nਕੀ ਤੁਸੀਂ ਇਸਨੂੰ ਇਨੇਬਲ ਕਰਨਾ ਚਾਹੁੰਦੇ ਹੋ?</string>
<string name="settings_category_backup_restore_title">ਬੈਕਅੱਪ ਅਤੇ ਰੀਸਟੋਰ</string>
<string name="reset_settings_title">ਸੈਟਿੰਗਾਂ ਨੂੰ ਰੀਸੈਟ ਕਰੋ</string>
<string name="reset_settings_summary">ਸਾਰੀਆਂ ਸੈਟਿੰਗਾਂ ਨੂੰ ਉਹਨਾਂ ਦੇ ਡਿਫ਼ਾਲਟ ਮੁੱਲਾਂ \'ਤੇ ਰੀਸੈਟ ਕਰੋ</string>
<string name="reset_all_settings">ਸਾਰੀਆਂ ਸੈਟਿੰਗਾਂ ਨੂੰ ਰੀਸੈੱਟ ਕਰਨ ਨਾਲ ਤੁਹਾਡੀਆਂ ਸਾਰੀਆਂ ਤਰਜੀਹੀ ਸੈਟਿੰਗਾਂ ਰੱਦ ਹੋ ਜਾਣਗੀਆਂ ਅਤੇ ਐਪ ਰੀਸਟਾਰਟ ਹੋ ਜਾਵੇਗਾ।
\n
\nਕੀ ਤੁਸੀਂ ਯਕੀਨੀ ਤੌਰ \'ਤੇ ਅੱਗੇ ਵਧਣਾ ਚਾਹੁੰਦੇ ਹੋ?</string>
<string name="error_insufficient_storage">ਡਿਵਾਈਸ \'ਤੇ ਲੋੜੀਂਦੀ ਖਾਲੀ ਥਾਂ ਨਹੀਂ ਹੈ</string>
<string name="no">ਨਹੀਂ</string>
</resources>

View File

@@ -836,15 +836,4 @@
<string name="show_more">Pokaż więcej</string>
<string name="show_less">Pokaż mniej</string>
<string name="notification_actions_summary_android13">Edytuj każdą poniższą akcję powiadomienia, naciskając ją. Pierwsze trzy akcje (odtwórz/wstrzymaj, poprzedni i następny) są ustawione przez system i nie można ich dostosować</string>
<string name="error_insufficient_storage">Za mało wolnego miejsca na urządzeniu</string>
<string name="settings_category_backup_restore_title">Kopia zapasowa i przywracanie</string>
<string name="reset_settings_summary">Resetuje wszystkie ustawienia do ich domyślnych wartości</string>
<string name="reset_settings_title">Resetuj ustawienia</string>
<string name="reset_all_settings">Zresetowanie wszystkich ustawień odrzuci wszystkie Twoje preferowane ustawienia i ponownie uruchomi aplikację.
\n
\nCzy na pewno chcesz kontynuować?</string>
<string name="yes">Tak</string>
<string name="no">Nie</string>
<string name="auto_update_check_description">NewPipe może od czasu do czasu automatycznie sprawdzać dostępność nowych wersji i powiadamiać Cię, gdy tylko będą dostępne.
\nCzy chcesz to włączyć?</string>
</resources>

View File

@@ -17,8 +17,8 @@
<string name="delete">Excluir</string>
<string name="detail_dislikes_img_view_description">Não gostei</string>
<string name="detail_likes_img_view_description">Curtidas</string>
<string name="download">Download</string>
<string name="download_dialog_title">Download</string>
<string name="download">Baixar</string>
<string name="download_dialog_title">Baixar</string>
<string name="error_details_headline">Detalhes:</string>
<string name="error_report_button_text">Relatar por e-mail</string>
<string name="error_report_title">Relatório de erro</string>
@@ -31,7 +31,7 @@
<string name="msg_error">Erro</string>
<string name="msg_copied">Copiado para a área de transferência</string>
<string name="msg_name">Nome do arquivo</string>
<string name="msg_running_detail">Toque para mais detalhes</string>
<string name="msg_running_detail">Toque para detalhes</string>
<string name="msg_running">O NewPipe está baixando</string>
<string name="msg_wait">Por favor, espere…</string>
<string name="network_error">Erro de rede</string>
@@ -57,30 +57,30 @@
<string name="downloads">Downloads</string>
<string name="downloads_title">Downloads</string>
<string name="did_you_mean">Você quis dizer \"%1$s\"\?</string>
<string name="app_ui_crash">O aplicativo travou</string>
<string name="app_ui_crash">Aplicativo/IU parou</string>
<string name="background_player_playing_toast">Reproduzindo em segundo plano</string>
<string name="could_not_setup_download_menu">O menu de download não pôde ser configurado</string>
<string name="detail_thumbnail_view_description">Reproduzir vídeo, duração:</string>
<string name="detail_uploader_thumbnail_view_description">Foto de perfil do autor</string>
<string name="detail_uploader_thumbnail_view_description">Miniatura do avatar do uploader</string>
<string name="download_path_audio_dialog_title">Escolha a pasta de download para arquivos de áudio</string>
<string name="download_path_audio_summary">Áudios baixados são salvos aqui</string>
<string name="download_path_audio_title">Pasta para áudios baixados</string>
<string name="download_path_dialog_title">Escolha a pasta de download para arquivos de vídeo</string>
<string name="download_path_summary">Vídeos baixados são salvos aqui</string>
<string name="download_path_title">Pasta para vídeos baixados</string>
<string name="kore_not_found">Instalar Kore?</string>
<string name="kore_not_found">Instalar o aplicativo Kore\?</string>
<string name="main_bg_subtitle">Toque na lupa para começar.</string>
<string name="msg_threads">Processos</string>
<string name="msg_threads">Threads</string>
<string name="no_available_dir">Por favor, defina uma pasta de download depois nas configurações</string>
<string name="no_player_found">Player de vídeo não encontrado. Instalar VLC?</string>
<string name="no_player_found">Sem reprodutor de transmissão. Instalar VLC?</string>
<string name="parsing_error">O site não pôde ser analisado</string>
<string name="play_audio">Áudio</string>
<string name="play_with_kodi_title">Reproduzir no Kodi</string>
<string name="play_with_kodi_title">Reproduzir com Kodi</string>
<string name="search">Pesquisar</string>
<string name="show_play_with_kodi_summary">Mostrar opção para reproduzir o vídeo no Kodi</string>
<string name="use_external_audio_player_title">Usar player de áudio externo</string>
<string name="use_external_video_player_title">Usar player de vídeo externo</string>
<string name="show_play_with_kodi_title">Mostrar opção \"Reproduzir no Kodi\"</string>
<string name="show_play_with_kodi_summary">Mostra uma opção para ver vídeo pelo media center do Kodi</string>
<string name="use_external_audio_player_title">Usar reprodutor de áudio externo</string>
<string name="use_external_video_player_title">Usar reprodutor de vídeo externo</string>
<string name="show_play_with_kodi_title">Mostrar opção \"Reproduzir com Kodi\"</string>
<string name="info_labels">O que aconteceu:\\nPedido:\\nIdioma do conteúdo:\\nPaís do conteúdo:\\nIdioma do aplicativo:\\nServiço:\\nHora GMT:\\nPacote:\\nVersão:\\nVersão do SO:</string>
<string name="open_in_popup_mode">Abrir no modo Popup</string>
<string name="default_popup_resolution_title">Resolução padrão do Popup</string>
@@ -88,11 +88,11 @@
<string name="show_higher_resolutions_summary">Apenas alguns dispositivos suportam vídeos em 2K/4K</string>
<string name="default_video_format_title">Formato de vídeo padrão</string>
<string name="popup_playing_toast">Reproduzindo em modo Popup</string>
<string name="all">Todos</string>
<string name="all">Tudo</string>
<string name="disabled">Desativado</string>
<string name="short_thousand">mil</string>
<string name="short_million">mi</string>
<string name="short_billion">bi</string>
<string name="short_thousand">k</string>
<string name="short_million">M</string>
<string name="short_billion">Bi</string>
<string name="msg_popup_permission">Essa permissão é necessária
\npara abrir em modo Popup</string>
<string name="clear">Limpar</string>
@@ -100,7 +100,7 @@
<string name="controls_background_title">Segundo plano</string>
<string name="popup_remember_size_pos_title">Lembrar propriedades do Popup</string>
<string name="popup_remember_size_pos_summary">Lembrar último tamanho e posição do Popup</string>
<string name="use_external_video_player_summary">Remove áudio em algumas resoluções</string>
<string name="use_external_video_player_summary">Remove o som em algumas resoluções</string>
<string name="show_search_suggestions_title">Sugestões de pesquisa</string>
<string name="show_search_suggestions_summary">Escolha as sugestões a serem exibidas enquanto estiver buscando</string>
<string name="best_resolution">Melhor resolução</string>
@@ -115,7 +115,7 @@
<string name="contribution_title">Colaborar</string>
<string name="copyright" formatted="true">© %1$s %2$s protegido pela licença %3$s</string>
<string name="title_activity_about">Sobre o NewPipe</string>
<string name="settings_category_downloads_title">Download</string>
<string name="settings_category_downloads_title">Baixar</string>
<string name="settings_file_charset_title">Caracteres permitidos em nome de arquivos</string>
<string name="settings_file_replacement_character_summary">Os caracteres inválidos são substituídos por este valor</string>
<string name="settings_file_replacement_character_title">Caractere de substituição</string>
@@ -124,20 +124,20 @@
<string name="subscribe_button_title">Inscrever-se</string>
<string name="subscribed_button_title">Inscrito</string>
<string name="channel_unsubscribed">Inscrição cancelada</string>
<string name="subscription_change_failed">Não foi possível alterar a inscrição</string>
<string name="subscription_update_failed">Não foi possível atualizar a inscrição</string>
<string name="subscription_change_failed">Não foi possível alterar inscrição</string>
<string name="subscription_update_failed">Não foi possível atualizar inscrição</string>
<string name="tab_subscriptions">Inscrições</string>
<string name="fragment_feed_title">Novidades</string>
<string name="resume_on_audio_focus_gain_title">Continuar reprodução</string>
<string name="resume_on_audio_focus_gain_summary">Continua vídeo após interrupções (ex: ligações)</string>
<string name="enable_search_history_title">Histórico de pesquisa</string>
<string name="enable_search_history_summary">Armazena histórico de pesquisa localmente</string>
<string name="enable_watch_history_title">Histórico de exibição</string>
<string name="enable_watch_history_summary">Mantenha o controle dos vídeos assistidos</string>
<string name="enable_watch_history_title">Histórico de vídeo</string>
<string name="enable_watch_history_summary">Armazena histórico de vídeos assistidos</string>
<string name="title_activity_history">Histórico</string>
<string name="action_history">Histórico</string>
<string name="notification_channel_name">Notificação do NewPipe</string>
<string name="notification_channel_description">Notificações para o player NewPipe</string>
<string name="notification_channel_description">Notificações para o reprodutor do NewPipe</string>
<string name="settings_category_player_behavior_title">Comportamento</string>
<string name="settings_category_history_title">Histórico e cache</string>
<string name="undo">Desfazer</string>
@@ -160,29 +160,29 @@
<item quantity="many">%s vídeos</item>
<item quantity="other">%s vídeos</item>
</plurals>
<string name="settings_category_player_title">Player</string>
<string name="settings_category_player_title">Reprodutor</string>
<string name="empty_list_subtitle">Nada aqui além de grilos</string>
<string name="delete_item_search_history">Deseja excluir este item do histórico de busca\?</string>
<string name="main_page_content">Conteúdo da página inicial</string>
<string name="blank_page_summary">Página em branco</string>
<string name="kiosk_page_summary">Página do Kiosk</string>
<string name="channel_page_summary">Página do canal</string>
<string name="kiosk_page_summary">Página do Quiosque</string>
<string name="channel_page_summary">Página de canais</string>
<string name="select_a_channel">Selecione um canal</string>
<string name="no_channel_subscribed_yet">Nenhuma inscrição ainda</string>
<string name="select_a_kiosk">Selecione um Kiosk</string>
<string name="select_a_kiosk">Selecione uma banca</string>
<string name="trending">Em Alta</string>
<string name="top_50">Top 50</string>
<string name="new_and_hot">Novos e tendências</string>
<string name="show_hold_to_append_title">Mostrar dica \"Segure para pôr na fila\"</string>
<string name="show_hold_to_append_summary">Mostra dica ao tocar no botão segundo plano ou Popup em \"Detalhes:\" do vídeo</string>
<string name="play_all">Reproduzir tudo</string>
<string name="player_stream_failure">Não é possível reproduzir este vídeo</string>
<string name="player_unrecoverable_failure">Ocorreu um erro irrecuperável na reprodução</string>
<string name="player_recoverable_failure">Se recuperando de um erro durante a reprodução</string>
<string name="player_stream_failure">Não é possível reproduzir esta transmissão</string>
<string name="player_unrecoverable_failure">Ocorreu um erro irrecuperável no reprodutor</string>
<string name="player_recoverable_failure">Se recuperando do erro do reprodutor</string>
<string name="play_queue_remove">Remover</string>
<string name="play_queue_stream_detail">Detalhes</string>
<string name="play_queue_audio_settings">Configurações de áudio</string>
<string name="hold_to_append">Toque longo para pôr na fila</string>
<string name="hold_to_append">Segure para pôr na fila</string>
<string name="unknown_content">[Desconhecido]</string>
<string name="start_here_on_background">Reproduzir em segundo plano</string>
<string name="start_here_on_popup">Reproduzir em um Popup</string>
@@ -191,52 +191,52 @@
<string name="give_back">Retribuir</string>
<string name="website_title">Site oficial</string>
<string name="website_encouragement">Visite o site do NewPipe para mais informações e novidades.</string>
<string name="no_player_found_toast">Player de vídeo não encontrado (você pode instalar o VLC para reproduzi-lo).</string>
<string name="no_player_found_toast">Reprodutor de transmissão não encontrado (pode instalar o VLC para assistir).</string>
<string name="default_content_country_title">País padrão do conteúdo</string>
<string name="always">Sempre</string>
<string name="just_once">Apenas uma vez</string>
<string name="just_once">Uma vez</string>
<string name="switch_to_background">Mudar para segundo plano</string>
<string name="switch_to_popup">Mudar para Popup</string>
<string name="switch_to_main">Mudar para principal</string>
<string name="external_player_unsupported_link_type">Players externos não suportam estes tipos de URL</string>
<string name="external_player_unsupported_link_type">Reprodutores externos não suportam estes tipos de URL</string>
<string name="video_streams_empty">Nenhuma transmissão de vídeo encontrada</string>
<string name="audio_streams_empty">Nenhuma transmissão de áudio encontrada</string>
<string name="video_player">Player de vídeo</string>
<string name="background_player">Reprodução em segundo plano</string>
<string name="popup_player">Reprodução em Popup</string>
<string name="video_player">Reprodutor de vídeo</string>
<string name="background_player">Reprodutor em segundo plano</string>
<string name="popup_player">Reprodutor Popup</string>
<string name="preferred_player_fetcher_notification_title">Obtendo informação…</string>
<string name="preferred_player_fetcher_notification_message">Carregando conteúdo solicitado</string>
<string name="import_data_title">Importar base de dados</string>
<string name="export_data_title">Exportar base de dados</string>
<string name="import_data_summary">Substitui seu histórico atual, inscrições, playlists e (opcionalmente) configurações</string>
<string name="export_data_summary">Exporta histórico, inscrições, playlists e configurações</string>
<string name="import_data_summary">Substitui seu histórico atual, inscrições, listas de reprodução e (opcionalmente) configurações</string>
<string name="export_data_summary">Exporta histórico, inscrições, listas de reprodução e configurações</string>
<string name="export_complete_toast">Exportado</string>
<string name="import_complete_toast">Importado</string>
<string name="no_valid_zip_file">Nenhum arquivo ZIP válido</string>
<string name="could_not_import_all_files">Aviso: Não foi possível importar todos os arquivos.</string>
<string name="override_current_data">Isso irá sobrescrever suas configurações atuais.</string>
<string name="controls_download_desc">Baixar arquivo</string>
<string name="controls_download_desc">Baixar arquivo de transmissão</string>
<string name="show_info">Mostrar informação</string>
<string name="tab_bookmarks">Playlists favoritas</string>
<string name="tab_bookmarks">Listas de reprodução favoritas</string>
<string name="controls_add_to_playlist_title">Adicionar a</string>
<string name="detail_drag_description">Arraste para ordenar</string>
<string name="create">Criar</string>
<string name="dismiss">Descartar</string>
<string name="dismiss">Dispensar</string>
<string name="rename">Renomear</string>
<string name="title_last_played">Última reprodução</string>
<string name="title_most_played">Mais assistidos</string>
<string name="title_last_played">Último reproduzido</string>
<string name="title_most_played">Mais reproduzido</string>
<string name="always_ask_open_action">Sempre perguntar</string>
<string name="create_playlist">Nova playlist</string>
<string name="create_playlist">Nova lista de reprodução</string>
<string name="rename_playlist">Renomear</string>
<string name="name">Nome</string>
<string name="add_to_playlist">Adicionar à playlist</string>
<string name="set_as_playlist_thumbnail">Definir como miniatura da playlist</string>
<string name="bookmark_playlist">Salvar como playlist favorita</string>
<string name="add_to_playlist">Adicionar à lista de reprodução</string>
<string name="set_as_playlist_thumbnail">Definir como miniatura da lista de reprodução</string>
<string name="bookmark_playlist">Favoritar lista de reprodução</string>
<string name="unbookmark_playlist">Remover dos favoritos</string>
<string name="delete_playlist_prompt">Excluir esta playlist?</string>
<string name="playlist_creation_success">Playlist criada</string>
<string name="playlist_add_stream_success">Adicionado à playlist</string>
<string name="playlist_thumbnail_change_success">Miniatura da playlist alterada.</string>
<string name="delete_playlist_prompt">Excluir esta lista de reprodução?</string>
<string name="playlist_creation_success">lista de reprodução criada</string>
<string name="playlist_add_stream_success">Adicionado à lista de reprodução</string>
<string name="playlist_thumbnail_change_success">Miniatura da lista de reprodução alterada.</string>
<string name="caption_none">Sem legendas</string>
<string name="resize_fit">Ajustar</string>
<string name="resize_fill">Preencher</string>
@@ -246,11 +246,11 @@
<string name="enable_disposed_exceptions_title">Reportar erros de out-of-lifecycle</string>
<string name="enable_disposed_exceptions_summary">Forçar entrega de relatórios de erros Rx fora de um fragmento ou atividade de lifecycle após o descarte</string>
<string name="use_inexact_seek_title">Usar busca de posição rápida (inexata)</string>
<string name="use_inexact_seek_summary">A busca inexata permite que o player de vídeo ache posições mais rápido com a precisão reduzida. Não funciona para voltar ou avançar 5, 15 ou 25 segundos</string>
<string name="use_inexact_seek_summary">A busca inexata permite que o reprodutor de vídeo ache posições mais rápido com a precisão reduzida. Não funciona para voltar ou avançar 5, 15 ou 25 segundos</string>
<string name="auto_queue_title">Enfileirar a próxima transmissão automaticamente</string>
<string name="auto_queue_summary">Continua a reprodução da fila (sem repetição) adicionando mais transmissões similares</string>
<string name="file">Arquivo</string>
<string name="invalid_directory">Essa pasta não existe</string>
<string name="invalid_directory">Pasta não existe</string>
<string name="invalid_source">Arquivo/fonte do conteúdo não existe</string>
<string name="invalid_file">O arquivo não existe ou não há permissão para leitura ou escrita</string>
<string name="file_name_empty_error">O nome do arquivo não pode ficar vazio</string>
@@ -262,78 +262,78 @@
<string name="export_ongoing">Exportando…</string>
<string name="import_file_title">Importar arquivo</string>
<string name="previous_export">Exportação anterior</string>
<string name="subscriptions_import_unsuccessful">Não foi possível importar as inscrições</string>
<string name="subscriptions_export_unsuccessful">Não foi possível exportar as inscrições</string>
<string name="import_youtube_instructions">Importar inscrições do YouTube em Google Takeout:
<string name="subscriptions_import_unsuccessful">Não foi possível importar inscrições</string>
<string name="subscriptions_export_unsuccessful">Não foi possível exportar inscrições</string>
<string name="import_youtube_instructions">Importe inscrições do YouTube pelo Google takeout:
\n
\n1. Acesse este URL: %1$s
\n2. Faça login quando solicitado
\n3. Clique em \"Todos os dados incluídos\", depois em \"Desmarcar todos\", selecione apenas \"Inscrições\" e clique em \"OK\"
\n4. Clique em \"Próxima etapa\" e, em seguida, em \"Criar exportação\"
\n5. Clique no botão \"Download\" quando ele aparecer
\n3. Clique em \"Todos os dados incluídos\", depois em \"Desmarcar todos\", em seguida, selecione apenas \"assinaturas\" e clique em \"OK\"
\n4. Clique em \"Próximo passo\" e em seguida, em \"Criar exportação\"
\n5. Clique no botão \"Baixar\" quando ele aparecer
\n6. Clique em IMPORTAR ARQUIVO abaixo e selecione o arquivo .zip baixado
\n7. [Se a importação do .zip falhar] Extraia o arquivo .csv (geralmente em \"YouTube e YouTube Music/subscriptions/subscriptions.csv\"), clique em IMPORTAR ARQUIVO abaixo e selecione o arquivo csv extraído</string>
<string name="import_soundcloud_instructions">Importar um perfil do SoundCloud digitando o URL ou seu ID:
\n7. Caso a importação do arquivo .zip falhe: Extraia o arquivo .csv (geralmente em \"YouTube e YouTube Music/subscriptions/subscriptions.csv\", clique em IMPORTAR ARQUIVO abaixo e selecione o arquivo csv extraído</string>
<string name="import_soundcloud_instructions">Importe um perfil do SoundCloud digitando o URL ou seu ID:
\n
\n1. Ative o \"Modo desktop (computador)\" em um navegador da Web (o site não está disponível para dispositivos móveis)
\n1. Ative o \"modo desktop\" em um navegador (o site não está disponível em aparelhos celulares)
\n2. Acesse este URL: %1$s
\n3. Faça login quando solicitado
\n4. Copie o URL do perfil para o qual você foi redirecionado.</string>
\n4. Copie o URL do perfil que você foi redirecionado.</string>
<string name="import_soundcloud_instructions_hint">seuID, soundcloud.com/seuid</string>
<string name="import_network_expensive_warning">Tenha em mente que esta operação poderá consumir muitos dados.
\n
\nVocê deseja continuar\?</string>
<string name="thumbnail_cache_wipe_complete_notice">Cache de imagens removidos</string>
<string name="thumbnail_cache_wipe_complete_notice">Cache de imagens limpo</string>
<string name="metadata_cache_wipe_title">Limpar cache de metadados</string>
<string name="metadata_cache_wipe_summary">Remove todos os dados de páginas em cache</string>
<string name="metadata_cache_wipe_complete_notice">Cache de metadados removidos</string>
<string name="playback_speed_control">Controles para velocidade de reprodução</string>
<string name="metadata_cache_wipe_complete_notice">Cache de metadados limpo</string>
<string name="playback_speed_control">Controles de velocidade de reprodução</string>
<string name="playback_tempo">Velocidade</string>
<string name="playback_pitch">Afinação</string>
<string name="unhook_checkbox">Desvincular (pode causar distorção)</string>
<string name="preferred_open_action_settings_title">Ação de \'abrir\' preferida</string>
<string name="preferred_open_action_settings_summary">Ação padrão ao abrir conteúdo — %s</string>
<string name="no_streams_available_download">Nenhum vídeo disponível para download</string>
<string name="no_streams_available_download">Nenhuma transmissão disponível para baixar</string>
<string name="drawer_open">Abrir gaveta</string>
<string name="drawer_close">Fechar gaveta</string>
<string name="caption_setting_title">Legendas</string>
<string name="caption_setting_description">Mudar tamanho da legenda e estilos de plano de fundo. Requer reiniciar o aplicativo para ter efeito</string>
<string name="clear_views_history_title">Limpar histórico de exibição</string>
<string name="clear_views_history_summary">Remove histórico de vídeos assistidos e as posições de reprodução</string>
<string name="delete_view_history_alert">Remover todo o histórico de exibição?</string>
<string name="watch_history_deleted">Histórico de exibição removido</string>
<string name="clear_search_history_title">Remover histórico de pesquisas</string>
<string name="clear_search_history_summary">Remove histórico de pesquisas</string>
<string name="delete_search_history_alert">Remover todo histórico de pesquisas?</string>
<string name="search_history_deleted">Histórico de pesquisa removido</string>
<string name="clear_views_history_title">Excluir histórico de vídeo</string>
<string name="clear_views_history_summary">Exclui o histórico de transmissões exibidas e as posições de reprodução</string>
<string name="delete_view_history_alert">Excluir todo o histórico de vídeo\?</string>
<string name="watch_history_deleted">Histórico de vídeos excluído</string>
<string name="clear_search_history_title">Excluir histórico de pesquisa</string>
<string name="clear_search_history_summary">Exclui o histórico de palavras-chave de pesquisa</string>
<string name="delete_search_history_alert">Excluir todo o histórico de pesquisa\?</string>
<string name="search_history_deleted">Histórico de busca limpo</string>
<string name="one_item_deleted">1 item excluído.</string>
<string name="app_license">NewPipe é um copyleft de software livre: Você pode usar, estudar, compartilhar e melhorar a seu gosto. Especificamente você pode redistribuir e/ou modificá-lo sob os termos da GNU General Public License como publicado pela Fundação de Software Livre, na versão 3 da Licença, ou (a seu critério) qualquer versão posterior.</string>
<string name="import_settings">Você também quer importar as configurações?</string>
<string name="privacy_policy_title">Política de privacidade do NewPipe</string>
<string name="privacy_policy_encouragement">O projeto NewPipe leva sua privacidade muito a sério. Por isso, o aplicativo não coleta nenhum dado sem seu consentimento.
\nA política de privacidade do NewPipe explica em detalhes quais dados são envidados e salvos quando você manda um relatório de erro.</string>
<string name="read_privacy_policy">Ver política de privacidade</string>
<string name="read_privacy_policy">Ler a política de privacidade</string>
<string name="start_accept_privacy_policy">A fim de cumprir com o Regulamento Geral sobre a Proteção de Dados da UE (RGPD), chamamos sua atenção para a política de privacidade do NewPipe. Por favor, leia com atenção.
\nVocê deve aceitá-la para nos enviar o relatório de erros.</string>
<string name="accept">Aceitar</string>
<string name="decline">Recusar</string>
<string name="limit_data_usage_none_description">Ilimitado</string>
<string name="limit_mobile_data_usage_title">Limitar resolução de vídeos ao usar dados móveis</string>
<string name="minimize_on_exit_title">Minimizar ao mudar de aplicativos</string>
<string name="minimize_on_exit_summary">Ação ao mudar de aplicativo a partir do player principal - %s</string>
<string name="limit_mobile_data_usage_title">Limitar a resolução quando estiver usando dados móveis</string>
<string name="minimize_on_exit_title">Minimizar ao trocar entre aplicativos</string>
<string name="minimize_on_exit_summary">Ação ao mudar para outro aplicativo a partir do reprodutor de vídeo principal %s</string>
<string name="minimize_on_exit_none_description">Nenhum</string>
<string name="minimize_on_exit_background_description">Minimizar reprodução para o modo de segundo plano</string>
<string name="minimize_on_exit_popup_description">Minimizar reprodução para o modo Popup</string>
<string name="minimize_on_exit_background_description">Minimizar para segundo plano</string>
<string name="minimize_on_exit_popup_description">Minimizar para reprodutor Popup</string>
<string name="skip_silence_checkbox">Avançar durante o silêncio</string>
<string name="playback_step">Passo</string>
<string name="playback_reset">Redefinir</string>
<string name="channels">Canais</string>
<string name="playlists">Playlists</string>
<string name="playlists">Listas de reprodução</string>
<string name="tracks">Faixas</string>
<string name="users">Usuários</string>
<string name="unsubscribe">Cancelar inscrição</string>
<string name="tab_choose">Escolha a guia</string>
<string name="settings_category_debug_title">Depuração</string>
<string name="tab_choose">Selecionar aba</string>
<string name="settings_category_debug_title">Debug</string>
<string name="settings_category_updates_title">Atualizações</string>
<string name="events">Eventos</string>
<string name="file_deleted">Arquivo excluído</string>
@@ -341,26 +341,26 @@
<string name="app_update_notification_channel_description">Notificações para novas versões do NewPipe</string>
<string name="download_to_sdcard_error_title">Armazenamento externo indisponível</string>
<string name="download_to_sdcard_error_message">Não é possível baixar para o cartão SD externo. Redefinir o local da pasta de download\?</string>
<string name="saved_tabs_invalid_json">Não foi possível ler as guias salvas, portanto, usamos as guias padrão</string>
<string name="restore_defaults">Restaurar configurações</string>
<string name="restore_defaults_confirmation">Deseja restaurar os padrões?</string>
<string name="saved_tabs_invalid_json">Não foi possível carregar as abas salvas, carregando as abas padrão</string>
<string name="restore_defaults">Restaurar padrões</string>
<string name="restore_defaults_confirmation">Deseja restaurar padrões\?</string>
<string name="subscribers_count_not_available">Número de inscritos indisponível</string>
<string name="main_page_content_summary">Quais guias são exibidas na página inicial</string>
<string name="main_page_content_summary">Que abas são visíveis na página inicial</string>
<string name="conferences">Conferências</string>
<string name="updates_setting_title">Atualizações</string>
<string name="updates_setting_description">Notificar quando uma nova versão do aplicativo estiver disponível</string>
<string name="list_view_mode">Modo de exibição da lista</string>
<string name="list_view_mode">Modo de exibição em lista</string>
<string name="list">Lista</string>
<string name="grid">Grade</string>
<string name="auto">Automático</string>
<string name="app_update_available_notification_title">Uma atualização do NewPipe está disponível!</string>
<string name="app_update_available_notification_title">Atualização do NewPipe disponível!</string>
<string name="missions_header_finished">Finalizado</string>
<string name="paused">pausado</string>
<string name="queued">na fila</string>
<string name="post_processing">pós-processamento</string>
<string name="enqueue">Colocar na fila</string>
<string name="enqueue">Fila</string>
<string name="permission_denied">Ação negada pelo sistema</string>
<string name="download_failed">Download falhou</string>
<string name="download_failed">O download falhou</string>
<string name="generate_unique_name">Gerar nome único</string>
<string name="overwrite">Sobrescrever</string>
<string name="overwrite_finished_warning">Um arquivo baixado com esse nome já existe</string>
@@ -368,11 +368,11 @@
<string name="show_error">Mostrar erro</string>
<string name="error_file_creation">O arquivo não pode ser criado</string>
<string name="error_path_creation">A pasta de destino não pode ser criada</string>
<string name="error_ssl_exception">Não foi possível estabelecer uma conexão segura</string>
<string name="error_unknown_host">Não foi possível encontrar o servidor</string>
<string name="error_ssl_exception">Uma conexão segura não pôde ser estabelecida</string>
<string name="error_unknown_host">O servidor não pôde ser encontrado</string>
<string name="error_connect_host">Não foi possível se conectar ao servidor</string>
<string name="error_http_no_content">O servidor não envia dados</string>
<string name="error_http_unsupported_range">O servidor não aceita downloads multi-processo, tente novamente com @string/msg_threads = 1</string>
<string name="error_http_unsupported_range">O servidor não aceita downloads em multi-thread, tente de novo com @string/msg_threads = 1</string>
<string name="error_http_not_found">Não encontrado</string>
<string name="error_postprocessing_failed">Falha no pós-processamento</string>
<string name="stop">Parar</string>
@@ -392,7 +392,7 @@
<string name="enable_playback_state_lists_title">Posições em listas</string>
<string name="enable_playback_state_lists_summary">Mostra indicadores de posição de reprodução em listas</string>
<string name="settings_category_clear_data_title">Excluir dados</string>
<string name="watch_history_states_deleted">Posições de reprodução removidas</string>
<string name="watch_history_states_deleted">Posições de reprodução limpas</string>
<string name="missing_file">Arquivo movido ou excluído</string>
<string name="overwrite_unrelated_warning">Já existe um arquivo com este nome</string>
<string name="overwrite_failed">O arquivo não pode ser sobrescrito</string>
@@ -403,24 +403,24 @@
<string name="error_timeout">Tempo limite de conexão</string>
<string name="confirm_prompt">Excluir todo o histórico de downloads ou excluir todos os arquivos baixados\?</string>
<string name="enable_queue_limit">Limitar fila de downloads</string>
<string name="enable_queue_limit_desc">Permitir apenas um download de cada vez</string>
<string name="enable_queue_limit_desc">Faz downloads um de cada vez</string>
<string name="start_downloads">Iniciar downloads</string>
<string name="pause_downloads">Pausar downloads</string>
<string name="downloads_storage_ask_title">Perguntar onde salvar o arquivo</string>
<string name="downloads_storage_ask_summary">Você será questionado onde salvar cada download.
\nAtive o seletor de pasta do sistema (SAF) se você quiser baixar em um cartão SD externo</string>
<string name="downloads_storage_use_saf_title">Usar o seletor de pastas do sistema (SAF)</string>
<string name="downloads_storage_use_saf_summary">A \"Estrutura de acesso ao armazenamento\" permite baixar em um cartão SD externo</string>
<string name="clear_playback_states_title">Remover posições de reprodução</string>
<string name="clear_playback_states_summary">Remove todas as posições de reprodução</string>
<string name="delete_playback_states_alert">Remover todas as posições de reprodução?</string>
<string name="drawer_header_description">Alternar serviço, atualmente selecionado:</string>
<string name="default_kiosk_page_summary">Kiosk padrão</string>
<string name="no_one_watching">Ninguém está assistindo</string>
<string name="downloads_storage_use_saf_summary">O \'Storage Access Framework\' permite baixar em um cartão SD externo</string>
<string name="clear_playback_states_title">Excluir posição das reproduções</string>
<string name="clear_playback_states_summary">Exclui todas as posições de reprodução</string>
<string name="delete_playback_states_alert">Excluir todas as posições de reprodução\?</string>
<string name="drawer_header_description">Alternar serviço, selecionados:</string>
<string name="default_kiosk_page_summary">Quiosque Padrão</string>
<string name="no_one_watching">Ninguém está vendo</string>
<plurals name="watching">
<item quantity="one">%s assistindo</item>
<item quantity="many">%s assistindo</item>
<item quantity="other">%s assistindo</item>
<item quantity="other">%s estão vendo</item>
</plurals>
<string name="no_one_listening">Ninguém está ouvindo</string>
<plurals name="listening">
@@ -429,14 +429,14 @@
<item quantity="other">%s ouvintes</item>
</plurals>
<string name="localization_changes_requires_app_restart">O idioma será alterado após reiniciar o aplicativo</string>
<string name="seek_duration_title">Duração de avanço/retrocesso rápido</string>
<string name="peertube_instance_url_title">Instâncias PeerTube</string>
<string name="peertube_instance_url_summary">Selecione suas instâncias favoritas do PeerTube</string>
<string name="seek_duration_title">Duração do salto para avançar/retroceder</string>
<string name="peertube_instance_url_title">Instâncias do PeerTube</string>
<string name="peertube_instance_url_summary">Escolha suas instâncias do PeerTube favoritas</string>
<string name="peertube_instance_url_help">Encontre as instâncias que gosta em %s</string>
<string name="peertube_instance_add_title">Adicionar instância</string>
<string name="peertube_instance_add_help">Insira o URL da instância</string>
<string name="peertube_instance_add_fail">Erro ao validar a instância</string>
<string name="peertube_instance_add_https_only">Somente URL HTTPS são compatíveis</string>
<string name="peertube_instance_add_https_only">Apenas os URL HTTPS são suportados</string>
<string name="peertube_instance_add_exists">A instância já existe</string>
<string name="local">Local</string>
<string name="recently_added">Adicionado recentemente</string>
@@ -447,7 +447,7 @@
<string name="choose_instance_prompt">Escolha uma instância</string>
<string name="clear_download_history">Limpar histórico de downloads</string>
<string name="delete_downloaded_files">Excluir arquivos baixados</string>
<string name="permission_display_over_apps">Obter permissão para exibir sobre outros aplicativos</string>
<string name="permission_display_over_apps">Dar permissão para mostrar por cima de outros aplicativos</string>
<string name="app_language_title">Idioma do aplicativo</string>
<string name="systems_language">Padrão do sistema</string>
<string name="subtitle_activity_recaptcha">Toque em \"Pronto\" ao resolver</string>
@@ -521,34 +521,34 @@
<string name="restricted_video">Este vídeo tem restrição de idade.
\n
\nAtive \"%1$s\" nas configurações se quiser vê-lo.</string>
<string name="remove_watched_popup_yes_and_partially_watched_videos">Sim, e vídeos parcialmente assistidos</string>
<string name="remove_watched_popup_warning">Os vídeos que foram assistidos antes e depois de terem sidos adicionados à playlist serão removidos.
<string name="remove_watched_popup_yes_and_partially_watched_videos">Sim, e vídeos parcialmente vistos</string>
<string name="remove_watched_popup_warning">Os vídeos que foram vistos antes e depois de terem sidos adicionados à lista de reprodução serão removidos.
\nTem certeza? Esta ação não pode ser desfeita!</string>
<string name="remove_watched_popup_title">Remover vídeos assistidos?</string>
<string name="remove_watched">Remover assistidos</string>
<string name="remove_watched_popup_title">Remover vídeos vistos\?</string>
<string name="remove_watched">Remover vistos</string>
<string name="show_original_time_ago_summary">Textos originais dos serviços serão visíveis nos itens de transmissão</string>
<string name="show_original_time_ago_title">Mostrar tempo original nos itens</string>
<string name="youtube_restricted_mode_enabled_title">Ativar o \"Modo Restrito\" do YouTube</string>
<string name="video_detail_by">Por %s</string>
<string name="channel_created_by">Criado por %s</string>
<string name="detail_sub_channel_thumbnail_view_description">Foto de perfil do canal</string>
<string name="detail_sub_channel_thumbnail_view_description">Miniatura do avatar do canal</string>
<string name="feed_group_show_only_ungrouped_subscriptions">Mostrar apenas inscrições não agrupadas</string>
<string name="search_showing_result_for">Mostrando resultados para: %s</string>
<string name="no_playlist_bookmarked_yet">Ainda não há playlist favoritas</string>
<string name="playlist_page_summary">Página da playlist</string>
<string name="select_a_playlist">Selecione uma playlist</string>
<string name="error_report_open_github_notice">Verifique se o erro já foi informado. Ao informar erros duplicados, você nos toma o tempo que poderíamos dedicar a outras correções de erros.</string>
<string name="no_playlist_bookmarked_yet">Ainda não há listas de reprodução favoritas</string>
<string name="playlist_page_summary">Página da lista de reprodução</string>
<string name="select_a_playlist">Selecione uma lista de reprodução</string>
<string name="error_report_open_github_notice">Por favor verifique se uma issue discutindo este problema já existe. Ao criar tickets duplicados, você tira de nós um tempo no qual poderíamos estar usando para corrigir um bug real.</string>
<string name="error_report_open_issue_button_text">Reporte no GitHub</string>
<string name="copy_for_github">Copiar relatório formatado</string>
<string name="never">Nunca</string>
<string name="wifi_only">Apenas em Wi-Fi</string>
<string name="wifi_only">Apenas no Wi-Fi</string>
<string name="autoplay_summary">Iniciar reprodução automaticamente — %s</string>
<string name="title_activity_play_queue">Fila de reprodução</string>
<string name="title_activity_play_queue">Reproduzir fila</string>
<string name="unsupported_url_dialog_message">Não foi possível reconhecer a URL. Abrir com outro aplicativo\?</string>
<string name="auto_queue_toggle">Pôr na fila automaticamente</string>
<string name="clear_queue_confirmation_description">A fila de reprodução atual será substituída</string>
<string name="clear_queue_confirmation_summary">Mudar de um player para outro pode substituir sua fila</string>
<string name="clear_queue_confirmation_title">Pedir confirmação antes de limpar a fila</string>
<string name="clear_queue_confirmation_description">A fila do reprodutor ativo será substituída</string>
<string name="clear_queue_confirmation_summary">Mudar de um reprodutor de vídeo para outro pode substituir sua fila</string>
<string name="clear_queue_confirmation_title">Pedir confirmação antes de limpar uma fila</string>
<string name="notification_action_shuffle">Aleatório</string>
<string name="notification_action_buffering">Carregando</string>
<string name="notification_action_nothing">Nada</string>
@@ -560,8 +560,8 @@
<string name="notification_action_2_title">Terceiro botão de ação</string>
<string name="notification_action_1_title">Segundo botão de ação</string>
<string name="notification_action_0_title">Primeiro botão de ação</string>
<string name="notification_scale_to_square_image_summary">Ajustar miniatura de vídeo mostrada na notificação de 16:9 para 1:1</string>
<string name="notification_scale_to_square_image_title">Ajustar miniatura para a proporção de 1:1</string>
<string name="notification_scale_to_square_image_summary">Cortar a miniatura do vídeo mostrada na notificação da proporção 16:9 para 1:1</string>
<string name="notification_scale_to_square_image_title">Cortar a miniatura para a proporção de 1:1</string>
<string name="show_memory_leaks">Mostrar vazamentos de memória</string>
<string name="enqueued">Na fila</string>
<string name="enqueue_stream">Pôr na fila</string>
@@ -575,8 +575,8 @@
<string name="show_thumbnail_summary">Usar miniatura para o plano de fundo da tela de bloqueio e notificações</string>
<string name="show_thumbnail_title">Mostrar miniatura</string>
<string name="msg_calculating_hash">Calculando hash</string>
<string name="hash_channel_description">Notificações sobre o progresso do hashing de vídeo</string>
<string name="hash_channel_name">Notificar hash de vídeo</string>
<string name="hash_channel_description">Notificações para o progresso do hash do vídeo</string>
<string name="hash_channel_name">Notificação de hash do vídeo</string>
<string name="show_meta_info_summary">Desative para ocultar as caixas de informações de metadados com informações adicionais sobre o criador, conteúdo da transmissão ou uma solicitação de pesquisa</string>
<string name="show_meta_info_title">Mostrar informação de metadados</string>
<string name="recent">Recente</string>
@@ -605,8 +605,8 @@
<string name="auto_device_theme_title">Automático (tema do dispositivo)</string>
<string name="night_theme_title">Tema noturno</string>
<string name="show_channel_details">Mostrar detalhes do canal</string>
<string name="disable_media_tunneling_summary">Desative o túnel de mídia se aparecer uma tela preta ou se tiver travamento durante a reprodução do vídeo.</string>
<string name="disable_media_tunneling_title">Desativar túnel de mídia</string>
<string name="disable_media_tunneling_summary">Desative o tunelamento de mídia se aparecer uma tela preta ou se tiver engasgos durante a reprodução do vídeo.</string>
<string name="disable_media_tunneling_title">Desativar tunelamento de mídia</string>
<string name="metadata_privacy_internal">Interno</string>
<string name="metadata_privacy_private">Privado</string>
<string name="metadata_privacy_unlisted">Não Listado</string>
@@ -631,52 +631,52 @@
\nDeseja cancelar a inscrição neste canal\?</string>
<string name="feed_load_error_account_info">Não foi possível carregar o feed para \'%s\'.</string>
<string name="feed_load_error">Erro ao carregar o feed</string>
<string name="downloads_storage_use_saf_summary_api_29">A \"Estrutura de acesso ao armazenamento\" é compatível apenas com versões a partir do Android 10</string>
<string name="downloads_storage_use_saf_summary_api_29">O \'Storage Access Framework\' é compatível apenas com versões a partir do Android 10</string>
<string name="downloads_storage_ask_summary_no_saf_notice">Você será questionado onde salvar cada download</string>
<string name="no_dir_yet">Nenhuma pasta de download definida ainda, escolha a pasta de download padrão agora</string>
<string name="off">Desativado</string>
<string name="on">Ativado</string>
<string name="off">Desligado</string>
<string name="on">Ligado</string>
<string name="tablet_mode_title">Modo tablet</string>
<string name="dont_show">Não mostrar</string>
<string name="low_quality_smaller">Baixa qualidade (pior)</string>
<string name="high_quality_larger">Alta qualidade (melhor)</string>
<string name="low_quality_smaller">Baixa qualidade (menor)</string>
<string name="high_quality_larger">Alta qualidade (maior)</string>
<string name="seekbar_preview_thumbnail_title">Pré visualização da miniatura da barra de busca</string>
<string name="comments_are_disabled">Os comentários estão desabilitados</string>
<string name="mark_as_watched">Marcar como assistido</string>
<string name="mark_as_watched">Marcar como visto</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>
<item quantity="other">%1$s downloads excluídos</item>
<item quantity="one">%1$s download apagado</item>
<item quantity="many">%1$s downloads apagados</item>
<item quantity="other">%1$s downloads apagados</item>
</plurals>
<plurals name="download_finished_notification">
<item quantity="one">%s download concluído</item>
<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="show_image_indicators_title">Exibir indicadores com 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>
<string name="start_main_player_fullscreen_summary">Não inicie os vídeos no mini player, mas diretamente para o modo de tela cheia, se a rotação automática estiver bloqueada. Você ainda pode acessar o mini player saindo da tela cheia</string>
<string name="start_main_player_fullscreen_title">Iniciar player principal em tela cheia</string>
<string name="main_page_content_swipe_remove">Deslize items para remove-los</string>
<string name="start_main_player_fullscreen_summary">Não inicia os vídeos no reprodutor reduzido, mas muda direto para o modo de tela cheia, se a rotação automática estiver travada. Você ainda consegue acessar o reprodutor reduzido saindo da tela cheia</string>
<string name="start_main_player_fullscreen_title">Iniciar o reprodutor principal em tela cheia</string>
<string name="remote_search_suggestions">Sugestões de busca remotas</string>
<string name="local_search_suggestions">Sugestões de busca locais</string>
<string name="processing_may_take_a_moment">Processando… Pode demorar um pouco</string>
<string name="check_for_updates">Buscar atualizações</string>
<string name="manual_update_description">Verificar manualmente se há novas versões</string>
<string name="checking_updates_toast">Buscando atualizações…</string>
<string name="crash_the_player">Travar reprodução</string>
<string name="show_crash_the_player_title">Mostrar \"Travar reprodução\"</string>
<string name="show_crash_the_player_summary">Mostra uma opção para travar a reprodução</string>
<string name="check_for_updates">Procurar por atualizações</string>
<string name="manual_update_description">Procurar manualmente por novas versões</string>
<string name="checking_updates_toast">Procurando por atualizações…</string>
<string name="crash_the_player">Travar o reprodutor de vídeo</string>
<string name="show_crash_the_player_title">Mostrar \"Fechar o reprodutor\"</string>
<string name="show_crash_the_player_summary">Mostra uma opção de travamento ao usar o reprodutor</string>
<string name="feed_new_items">Novos itens do feed</string>
<string name="error_report_channel_name">Notificação de relatório de erro</string>
<string name="error_report_channel_description">Notificações para reportar erros</string>
<string name="error_report_notification_title">O NewPipe encontrou um erro, toque para relatar</string>
<string name="error_report_notification_title">O NewPipe encontrou um erro, toque para reportar</string>
<string name="create_error_notification">Crie uma notificação de erro</string>
<string name="no_appropriate_file_manager_message_android_10">Nenhum gerenciador de arquivos apropriado foi encontrado para esta ação.
\nInstale um gerenciador de arquivos compatível com a \"Estrutura de acesso ao armazenamento\"</string>
\nInstale um gerenciador de arquivos compatível com o Storage Access Framework</string>
<string name="error_report_notification_toast">Ocorreu um erro, consulte a notificação</string>
<string name="show_error_snackbar">Mostrar um snackbar de erro</string>
<string name="no_appropriate_file_manager_message">Nenhum gerenciador de arquivos apropriado foi encontrado para esta ação.
@@ -684,36 +684,36 @@
<string name="detail_pinned_comment_view_description">Comentário fixado</string>
<string name="leak_canary_not_available">O LeakCanary não está disponível</string>
<string name="progressive_load_interval_exoplayer_default">ExoPlayer padrão</string>
<string name="settings_category_player_notification_title">Notificação de reprodução</string>
<string name="settings_category_player_notification_summary">Configurar notificação da reprodução do vídeo atual</string>
<string name="settings_category_player_notification_title">Notificação do reprodutor</string>
<string name="settings_category_player_notification_summary">Configurar a notificação da reprodução da transmissão atual</string>
<string name="notifications">Notificações</string>
<string name="streams_notification_channel_name">Novos vídeos</string>
<string name="streams_notification_channel_description">Notificações sobre novos vídeos de inscrições</string>
<string name="enable_streams_notifications_title">Notificações sobre novos vídeos</string>
<string name="enable_streams_notifications_summary">Notificar sobre novos vídeos de inscrições</string>
<string name="streams_notification_channel_name">Novas transmissões</string>
<string name="streams_notification_channel_description">Notificações sobre novas transmissões para inscrições</string>
<string name="enable_streams_notifications_title">Notificações de novas transmissões</string>
<string name="enable_streams_notifications_summary">Notificar sobre novas transmissões de inscrições</string>
<string name="streams_notifications_interval_title">Frequência de verificação</string>
<string name="any_network">Nenhuma rede</string>
<string name="delete_downloaded_files_confirm">Excluir todos os arquivos baixados?</string>
<string name="delete_downloaded_files_confirm">Excluir todos os arquivos baixados do disco\?</string>
<string name="you_successfully_subscribed">Agora você se inscreveu neste canal</string>
<string name="toggle_all">Alternar tudo</string>
<string name="enumeration_comma">,</string>
<string name="loading_stream_details">Carregando detalhes da transmissão…</string>
<plurals name="new_streams">
<item quantity="one">%s novo vídeo</item>
<item quantity="many">%s novos vídeos</item>
<item quantity="other">%s novos vídeos</item>
<item quantity="one">%s nova transmissão</item>
<item quantity="many">%s novas transmissões</item>
<item quantity="other">%s novas transmissões</item>
</plurals>
<string name="check_new_streams">Buscar novos vídeos</string>
<string name="check_new_streams">Verifica por novas transmissões</string>
<string name="streams_notifications_network_title">Conexão de rede necessária</string>
<string name="notifications_disabled">As notificações estão desativadas</string>
<string name="get_notified">Seja notificado</string>
<string name="percent">Por cento</string>
<string name="semitone">Semitom</string>
<string name="selected_stream_external_player_not_supported">O vídeo selecionado não é compatível com players externos</string>
<string name="no_audio_streams_available_for_external_players">Nenhuma transmissão de áudio está disponível para players externos</string>
<string name="streams_not_yet_supported_removed">Os vídeos que ainda não são suportados pelo assistente de download não são exibidos</string>
<string name="no_video_streams_available_for_external_players">Nenhuma transmissão de vídeo está disponível para players externos</string>
<string name="select_quality_external_players">Selecione a qualidade para players externos</string>
<string name="selected_stream_external_player_not_supported">A transmissão selecionada não é compatível com reprodutores externos</string>
<string name="no_audio_streams_available_for_external_players">Nenhum transmissão de áudio está disponível para reprodutores externos</string>
<string name="streams_not_yet_supported_removed">Transmissões que ainda não são suportadas pelo baixador não são exibidos</string>
<string name="no_video_streams_available_for_external_players">Nenhum vídeo de transmissão está disponível para reprodutores externos</string>
<string name="select_quality_external_players">Selecione a qualidade para reprodutores externos</string>
<string name="unknown_format">Formato desconhecido</string>
<string name="unknown_quality">Qualidade desconhecida</string>
<string name="progressive_load_interval_title">Tamanho do intervalo de carregamento da reprodução</string>
@@ -722,32 +722,32 @@
<string name="faq_title">Perguntas frequentes</string>
<string name="sort">Classificar</string>
<string name="fast_mode">Modo rápido</string>
<string name="import_subscriptions_hint">Importar ou exportar inscrições no menu com 3 pontos</string>
<string name="import_subscriptions_hint">Importar ou exportar inscrições do menu de 3 pontos</string>
<string name="app_update_available_notification_text">Toque para baixar %s</string>
<string name="app_update_unavailable_toast">Você já possui a atualização mais recente do NewPipe</string>
<string name="app_update_unavailable_toast">Você está executando a versão mais recente do NewPipe</string>
<string name="night_theme_available">Esta opção só está disponível se %s for selecionado para Tema</string>
<string name="unset_playlist_thumbnail">Desativar miniatura permanente</string>
<string name="card">Cartão</string>
<string name="msg_failed_to_copy">Falha ao copiar para a área de transferência</string>
<string name="playlist_add_stream_success_duplicate">Duplicata adicionada %d vez(es)</string>
<string name="duplicate_in_playlist">As playlists em cinza já contêm este item.</string>
<string name="duplicate_in_playlist">As listas de reprodução em cinza já contêm este item.</string>
<string name="ignore_hardware_media_buttons_title">Ignorar eventos de botão de mídia de hardware</string>
<string name="ignore_hardware_media_buttons_summary">Útil, por exemplo, se você estiver usando um fone de ouvido com botões físicos quebrados</string>
<string name="remove_duplicates">Remover duplicados</string>
<string name="remove_duplicates_title">Remover duplicados\?</string>
<string name="remove_duplicates_message">Deseja remover todos os vídeos duplicados nesta playlist?</string>
<string name="feed_hide_streams_title">Mostrar próximos vídeos</string>
<string name="feed_show_hide_streams">Mostrar/ocultar vídeos</string>
<string name="remove_duplicates_message">Deseja remover todos as transmissões duplicadas nesta lista de reprodução?</string>
<string name="feed_hide_streams_title">Mostrar as transmissões seguintes</string>
<string name="feed_show_hide_streams">Mostrar/ocultar transmissões</string>
<string name="feed_show_partially_watched">Parcialmente assistido</string>
<string name="feed_show_upcoming">Em breve</string>
<string name="feed_show_watched">Totalmente assistido</string>
<string name="left_gesture_control_summary">Escolha o gesto para a parte esquerda na tela de reprodução</string>
<string name="left_gesture_control_title">Ação para o gesto à esquerda</string>
<string name="right_gesture_control_summary">Escolha o gesto para a parte direita na tela de reprodução</string>
<string name="left_gesture_control_summary">Escolha o gesto da mão esquerda da tela do reprodutor</string>
<string name="left_gesture_control_title">Ação do gesto esquerdo</string>
<string name="right_gesture_control_summary">Escolha o gesto da mão direita da tela do reprodutor</string>
<string name="brightness">Brilho</string>
<string name="volume">Volume</string>
<string name="none">Nenhum</string>
<string name="right_gesture_control_title">Ação para o gesto à direita</string>
<string name="right_gesture_control_title">Ação do gesto direito</string>
<string name="progressive_load_interval_summary">Altere o tamanho do intervalo de carregamento (atualmente %s). Um valor menor pode acelerar o carregamento inicial do vídeo</string>
<string name="prefer_original_audio_title">Dar preferência ao áudio original</string>
<string name="prefer_original_audio_summary">Selecionar o áudio original e independentemente do idioma</string>
@@ -755,69 +755,69 @@
<string name="prefer_descriptive_audio_summary">Selecionar um áudio com descrição para pessoas com dificuldades de visão, se disponível</string>
<string name="play_queue_audio_track">Áudio: %s</string>
<string name="audio_track">Faixa de áudio</string>
<string name="select_audio_track_external_players">Selecione a faixa de áudio para players externo</string>
<string name="select_audio_track_external_players">Seleciona faixa de áudio para reprodutor externo</string>
<string name="unknown_audio_track">Desconhecido</string>
<string name="settings_category_exoplayer_title">Configurar ExoPlayer</string>
<string name="settings_category_exoplayer_summary">Gerenciar algumas configurações do ExoPlayer. É necessário reiniciar o player para aplicar as mudanças</string>
<string name="settings_category_exoplayer_title">Configurações de ExoPlayer</string>
<string name="settings_category_exoplayer_summary">Gerenciar algumas configurações de ExoPlayer. É necessário reiniciar o reprodutor para aplicar as mudanças</string>
<string name="audio_track_name">%1$s %2$s</string>
<string name="audio_track_type_original">original</string>
<string name="audio_track_type_dubbed">dublado</string>
<string name="audio_track_type_descriptive">descritivo</string>
<string name="always_use_exoplayer_set_output_surface_workaround_summary">Esta solução alternativa libera os codificadores de vídeo quando ocorre uma alteração de superfície, no lugar de definir a superfície para o Codec diretamente. Já usado pelo ExoPlayer em alguns dispositivos com esse problema, essa configuração só tem efeito no Android 6 e superior
\n
\nAtivar esta opção pode evitar erros de reprodução ao alternar o player de vídeo atual ou alternar para tela cheia</string>
<string name="audio_track_present_in_video">Uma faixa de áudio já deve estar presente neste vídeo</string>
<string name="use_exoplayer_decoder_fallback_title">Usar decodificador alternativo do ExoPlayer</string>
<string name="always_use_exoplayer_set_output_surface_workaround_title">Usar sempre a solução alternativa de configuração da superfície de saída de vídeo do ExoPlayer</string>
\nAtivar esta opção pode evitar erros de reprodução ao alternar o reprodutor de vídeo atual ou alternar para tela cheia</string>
<string name="audio_track_present_in_video">Uma faixa de áudio já deve estar presente nesta transmissão</string>
<string name="use_exoplayer_decoder_fallback_title">Utilizar a contingência do decodificador do ExoPlayer</string>
<string name="always_use_exoplayer_set_output_surface_workaround_title">Sempre utilizar o configuração de saída de vídeo alternativa do ExoPlayer</string>
<string name="use_exoplayer_decoder_fallback_summary">Habilite essa opção se você tiver problemas de inicialização do decodificador, que retorna codificadores de baixa prioridade se o decodificador primário falhar. Isso pode resultar em pior desempenho de reprodução</string>
<string name="main_tabs_position_summary">Mover o seletor da guia principal para a parte inferior</string>
<string name="main_tabs_position_title">Posição de guias principais</string>
<string name="no_streams">Nenhum conteúdo</string>
<string name="main_tabs_position_summary">Mova o seletor da aba principal para a parte inferior</string>
<string name="main_tabs_position_title">Posição das abas principais</string>
<string name="no_streams">Nenhuma transmissão</string>
<string name="no_live_streams">Nenhuma transmissão ao vivo</string>
<string name="disable_media_tunneling_automatic_info">O túnel de mídia foi desabilitado por padrão em seu dispositivo porque seu modelo é conhecido por não suportá-lo.</string>
<string name="disable_media_tunneling_automatic_info">O tunelamento de mídia foi desabilitado por padrão em seu dispositivo porque seu modelo é conhecido por não suportá-lo.</string>
<string name="channel_tab_videos">Vídeos</string>
<string name="metadata_subscribers">Inscritos</string>
<string name="show_channel_tabs_summary">Quais guias são mostradas na página do canal</string>
<string name="show_channel_tabs">Guias do canal</string>
<string name="show_channel_tabs_summary">Quais guias são mostradas nas páginas do canal</string>
<string name="show_channel_tabs">Guias de canal</string>
<string name="channel_tab_shorts">Shorts</string>
<string name="loading_metadata_title">Carregando metadados…</string>
<string name="feed_fetch_channel_tabs">Buscar guias de canal</string>
<string name="channel_tab_about">Sobre</string>
<string name="channel_tab_albums">Álbuns</string>
<string name="feed_fetch_channel_tabs_summary">Guias a serem buscadas ao atualizar o feed. Esta opção não tem efeito se um canal for atualizado usando o modo rápido.</string>
<string name="channel_tab_playlists">Playlists</string>
<string name="channel_tab_playlists">Listas de reprodução</string>
<string name="channel_tab_tracks">Faixas</string>
<string name="channel_tab_channels">Canais</string>
<string name="channel_tab_livestreams">Ao vivo</string>
<string name="image_quality_title">Qualidade da imagem</string>
<string name="question_mark">\?</string>
<string name="share_playlist_with_list">Compartilhar URL</string>
<string name="share_playlist_with_titles">Compartilhar com título</string>
<string name="share_playlist_with_list">Compartilhar lista URL</string>
<string name="share_playlist_with_titles">Compartilhar com Títulos</string>
<string name="share_playlist_content_details">%1$s
\n%2$s</string>
<string name="toggle_screen_orientation">Alternar orientação da tela</string>
<string name="toggle_screen_orientation">Alterna a orientação da tela</string>
<string name="image_quality_low">Baixa qualidade</string>
<string name="toggle_fullscreen">Alternar tela cheia</string>
<string name="metadata_avatars">Fotos</string>
<string name="next_stream">Próxima vídeo</string>
<string name="metadata_subchannel_avatars">Fotos de perfil do subcanal</string>
<string name="open_play_queue">Abrir fila de reprodução</string>
<string name="metadata_avatars">Avatares</string>
<string name="next_stream">Próxima transmissão</string>
<string name="metadata_subchannel_avatars">Avatares do subcanal</string>
<string name="open_play_queue">Abrir a fila de reprodução</string>
<string name="image_quality_none">Não carregar imagens</string>
<string name="image_quality_high">Alta qualidade</string>
<string name="share_playlist">Compartilhar playlist</string>
<string name="share_playlist">Compartilhar Lista de Reprodução</string>
<string name="forward">Avançar</string>
<string name="rewind">Retroceder</string>
<string name="replay">Repetir</string>
<string name="share_playlist_with_titles_message">Compartilhar playlist com detalhes como o nome da playlist e títulos de vídeo ou como uma lista simples dos URL de vídeos</string>
<string name="replay">Repete</string>
<string name="share_playlist_with_titles_message">Compartilhar lista de reprodução com detalhes como nome da lista de reprodução e títulos de vídeo ou como uma lista simples dos URL de vídeos</string>
<string name="image_quality_medium">Qualidade média</string>
<string name="metadata_uploader_avatars">Fotos de perfil do autor</string>
<string name="metadata_uploader_avatars">Avatares do carregador</string>
<string name="video_details_list_item">- %1$s: %2$s</string>
<string name="image_quality_summary">Escolha a qualidade das imagens e se as imagens devem ser carregadas, para reduzir o uso de dados e memória. As alterações limpam o cache de imagens na memória e no disco - %s</string>
<string name="image_quality_summary">Escolha a qualidade das imagens ou se deve carregá-las como estão, para reduzir o uso de dados e memória. Alterações limpam a memória e o cache de imagem no disco %s</string>
<string name="play">Reproduzir</string>
<string name="more_options">Mais opções</string>
<string name="metadata_thumbnails">Miniaturas</string>
<string name="duration">Duração</string>
<string name="previous_stream">Vídeo anterior</string>
<string name="previous_stream">Transmissão anterior</string>
<string name="metadata_banners">Banners</string>
<string name="show_more">Mostrar mais</string>
<string name="notification_actions_summary_android13">Edite cada ação de notificação abaixo tocando nela. As três primeiras ações (reproduzir/pausar, anterior e seguinte) são definidas pelo sistema e não podem ser personalizadas.</string>
@@ -827,15 +827,4 @@
<item quantity="other">%s respostas</item>
</plurals>
<string name="show_less">Mostrar menos</string>
<string name="error_insufficient_storage">Não há espaço livre suficiente no dispositivo</string>
<string name="yes">Sim</string>
<string name="no">Não</string>
<string name="settings_category_backup_restore_title">Backup e restauração</string>
<string name="auto_update_check_description">O NewPipe pode verificar automaticamente se há novas versões de tempos em tempos e notificá-lo quando elas estiverem disponíveis.
\nDeseja ativar essa opção?</string>
<string name="reset_settings_title">Restaurar configurações</string>
<string name="reset_settings_summary">Restaurar todas as configurações para seus valores padrão</string>
<string name="reset_all_settings">A restauração de todas as configurações descartará todas as suas configurações preferidas e reiniciará o aplicativo.
\n
\nTem certeza de que deseja continuar?</string>
</resources>

View File

@@ -827,15 +827,4 @@
<item quantity="other">%s respostas</item>
</plurals>
<string name="show_less">Mostrar menos</string>
<string name="auto_update_check_description">O NewPipe pode verificar automaticamente se há novas versões de tempos em tempos e notificá-lo quando elas estiverem disponíveis.
\nDeseja ativar essa opção?</string>
<string name="reset_settings_summary">Repor valores originais de todas as definições</string>
<string name="reset_all_settings">A restauração de todas as configurações descartará todas as suas configurações preferidas e reiniciará a app.
\n
\nTem certeza que deseja continuar?</string>
<string name="yes">Sim</string>
<string name="no">Não</string>
<string name="settings_category_backup_restore_title">Backup e restauro</string>
<string name="reset_settings_title">Repor definições</string>
<string name="error_insufficient_storage">Não há espaço suficiente no aparelho</string>
</resources>

View File

@@ -827,15 +827,4 @@
</plurals>
<string name="show_less">Mostrar menos</string>
<string name="notification_actions_summary_android13">Edite cada ação de notificação abaixo a tocar nela. As três primeiras ações (reproduzir/pausa, anterior e seguinte) são definidas pelo sistema e não podem ser personalizadas.</string>
<string name="error_insufficient_storage">Não há espaço suficiente no dispositivo</string>
<string name="yes">Sim</string>
<string name="no">Não</string>
<string name="reset_settings_summary">Repor valores originais de todas as definições</string>
<string name="settings_category_backup_restore_title">Backup e restauro</string>
<string name="reset_settings_title">Repor definições</string>
<string name="reset_all_settings">A restauração de todas as configurações descartará todas as suas configurações preferidas e reiniciará a app.
\n
\nTem certeza que deseja continuar?</string>
<string name="auto_update_check_description">O NewPipe pode verificar automaticamente se há novas versões de tempos em tempos e notificá-lo quando elas estiverem disponíveis.
\nDeseja ativar essa opção?</string>
</resources>

View File

@@ -30,7 +30,7 @@
<string name="dark_theme_title">Întunecat</string>
<string name="light_theme_title">Luminos</string>
<string name="download_dialog_title">Descărcați</string>
<string name="show_next_and_similar_title">Arată videoclipurile care \'Urmează\' și cele \'Similare\'</string>
<string name="show_next_and_similar_title">Arată videoclipurile care \'Urmează\' şi cele \'Similare\'</string>
<string name="unsupported_url">URL nesuportat</string>
<string name="content_language_title">Limba dorită a conținutului</string>
<string name="settings_category_video_audio_title">Video și Audio</string>
@@ -154,7 +154,7 @@
<string name="copyright" formatted="true">© %1$s de %2$s sub %3$s</string>
<string name="tab_about">Despre &amp; FAQ</string>
<string name="tab_licenses">Licențe</string>
<string name="app_description">Un player de streaming „ușor” liber, pentru Android.</string>
<string name="app_description">Un player de streaming „uşor” liber, pentru Android.</string>
<string name="view_on_github">Vedeți pe GitHub</string>
<string name="app_license_title">Licența NewPipe</string>
<string name="contribution_encouragement">Fie că aveți idei de: traducere, modificări de design, curățare a codului sau modificări de cod cu adevărat importante - ajutorul este întotdeauna binevenit. Cu cât se face mai mult, cu atât mai bine devine!</string>
@@ -227,23 +227,23 @@
<string name="play_queue_remove">Eliminați</string>
<string name="play_queue_stream_detail">Detalii</string>
<string name="play_queue_audio_settings">Setări Audio</string>
<string name="hold_to_append">Apăsați pentru a adăuga în coadă</string>
<string name="hold_to_append">Apăsaţi pentru a adăuga în coadă</string>
<string name="start_here_on_background">Începeți redarea în fundal</string>
<string name="start_here_on_popup">Începeți redarea în popup</string>
<string name="drawer_open">Deschdeți sertarul</string>
<string name="drawer_close">Închideți sertarul</string>
<string name="preferred_open_action_settings_title">Opțiunea de deschidere preferată</string>
<string name="drawer_open">Deschdeţi sertarul</string>
<string name="drawer_close">Închideţi sertarul</string>
<string name="preferred_open_action_settings_title">Opţiunea de deschidere preferată</string>
<string name="preferred_open_action_settings_summary">Acțiune implicită la deschiderea conținutului - %s</string>
<string name="video_player">Player Video</string>
<string name="background_player">Player Fundal</string>
<string name="popup_player">Player Popup</string>
<string name="always_ask_open_action">Întrebați întotdeauna</string>
<string name="no_streams_available_download">Nu există fluxuri disponibile pentru descărcare</string>
<string name="detail_drag_description">Trageți pentru a reordona</string>
<string name="detail_drag_description">Trageţi pentru a reordona</string>
<string name="create">Creați</string>
<string name="dismiss">Respingeți</string>
<string name="rename">Redenumiți</string>
<string name="donation_title">Donați</string>
<string name="rename">Redenumiţi</string>
<string name="donation_title">Donaţi</string>
<string name="import_settings">De asemenea, doriți să importați setări?</string>
<string name="name">Nume</string>
<string name="playlist_creation_success">Listă de redare creată</string>
@@ -765,14 +765,15 @@
<string name="toggle_fullscreen">Schimbați pe ecran complet</string>
<string name="feed_fetch_channel_tabs">Preluați filele canalului</string>
<string name="metadata_avatars">Avatare</string>
<string name="use_exoplayer_decoder_fallback_summary">Activați această opțiune dacă aveți probleme cu inițializarea decodorului care trece înapoi la decodoare cu prioritate mai scăzută dacă inițializarea decodoarelor principale eșuează. Asta poate duce la performanță de redare mai slabă decât atunci când se utilizează decodoarele principale</string>
<string name="use_exoplayer_decoder_fallback_summary">Activați această opțiune dacă aveți probleme cu inițializarea decodorului care trece înapoi la decodoare cu prioritate mai scăzută dacă inițializarea decodoarelor principale eșuează. Asta poate duce la performanță de redare mai slabă decât atunci când se utilizează decodoarele principale.</string>
<string name="right_gesture_control_title">Acțiunea gestului din dreapta</string>
<string name="audio_track_name">%1$s %2$s</string>
<string name="always_use_exoplayer_set_output_surface_workaround_title">Utilizați întotdeauna configurarația suprafeței de ieșire video din ExoPlayer ca soluție alternativă</string>
<string name="next_stream">Transmisia viitoare</string>
<string name="disable_media_tunneling_automatic_info">Tunelizarea media a fost dezactivată în mod implicit pe dispozitivul dumneavoastră deoarece se cunoaște despre acest model de dispozitiv că nu o suportă.</string>
<string name="metadata_subchannel_avatars">Avatarele subcanalelor</string>
<string name="always_use_exoplayer_set_output_surface_workaround_summary">Această soluție alternativă eliberează și reinstanțează codecurile video când se întamplă o schimbare a suprafeței, în loc de a seta suprafața pentru codec direct. Deja folosită de ExoPlayer pe unele dispozitive cu această problemă, această setare are efect doar pe Android 6 sau mai mare
<string name="always_use_exoplayer_set_output_surface_workaround_summary">Această soluție alternativă eliberează și reinstanțează codecurile video când se întamplă o schimbare a suprafeței, în loc de a seta suprafața pentru codec direct. Deja
\n folosită de ExoPlayer pe unele dispozitive cu această problemă, această setare are efect doar pe Android 6 sau mai mare
\n
\nActivarea acestei opțiuni poate preveni erorile de redare când se schimbă playerul video curent sau se trece pe ecran complet</string>
<string name="audio_track_present_in_video">O coloană sonoră ar trebui să fie deja prezentă în această transmisie</string>
@@ -827,15 +828,4 @@
<string name="show_more">Arată mai multe</string>
<string name="show_less">Arată mai puține</string>
<string name="channel_tab_tracks">Piste</string>
<string name="error_insufficient_storage">Nu este suficient spațiu liber pe dispozitiv</string>
<string name="settings_category_backup_restore_title">Backup și restabilire</string>
<string name="auto_update_check_description">NewPipe poate verifica automat pentru versiuni noi din când în când și te poate notifica când acestea sunt disponibile.
\nDoriți să activați acest lucru?</string>
<string name="reset_settings_summary">Resetează toate setările la valorile inițiale</string>
<string name="yes">Da</string>
<string name="no">Nu</string>
<string name="reset_settings_title">Resetează setări</string>
<string name="reset_all_settings">Resetarea tuturor setărilor va elimina toate setările tale preferate și va reporni aplicația.
\n
\nSigur doriți să continuați?</string>
</resources>

View File

@@ -832,12 +832,4 @@
<item quantity="other">%s ответов</item>
</plurals>
<string name="notification_actions_summary_android13">Отредактируйте каждое действие уведомления ниже, нажав на него. Первые три действия (воспроизведение/пауза, предыдущее и следующее) задаются системой и не подлежат настройке.</string>
<string name="error_insufficient_storage">Недостаточно свободного места на устройстве</string>
<string name="reset_settings_summary">Сбросить все настройки на их значения по умолчанию</string>
<string name="yes">Да</string>
<string name="no">Нет</string>
<string name="settings_category_backup_restore_title">Резервное копирование и восстановление</string>
<string name="reset_settings_title">Сбросить настройки</string>
<string name="auto_update_check_description">NewPipe может автоматически проверять наличие обновлений и уведомить вас, когда они будут доступны.
\nЖелаете включить эту функцию?</string>
</resources>

View File

@@ -813,15 +813,4 @@
<string name="show_more">なーふぃんんーじゅん</string>
<string name="show_less">ひょうじいきらくすん</string>
<string name="notification_actions_summary_android13">いかぬちうちアクションタップしへんしゅうさびーん。さいしょぬみーちぬアクション (さいせい/いちじていし、めーんかい、ちぎんかい)ーシステムにゆってぃしっていさりてぃうぅい、カスタマイズすしぇーなやびらん。</string>
<string name="yes">はい</string>
<string name="auto_update_check_description">NewPipeーてぃんじちーがみーさるバージョンじちゃーてぃきんかいチェックしー、こうしんがのうないるとぅちうちさびーん。
\nゆうこうなさびーが</string>
<string name="error_insufficient_storage">デバイスぬあきゆういょうがふすくそーいびーん</string>
<string name="no">うぅーうぅー</string>
<string name="settings_category_backup_restore_title">バックアップとぅふくぎん</string>
<string name="reset_settings_title">しっていリセット</string>
<string name="reset_settings_summary">まじりぬしっていデフォルトじょうたいんかいリセットさびーん</string>
<string name="reset_all_settings">まじりぬしっていリセットしーねー、ゆーいるしんしっていぬまじりはちされい、アプリぬさいきちゃーさびーん。
\n
\nずっこうさびーが</string>
</resources>

View File

@@ -539,7 +539,7 @@
<string name="clear_queue_confirmation_summary">Colende dae unu riproduidore a s\'àteru dias pòdere remplasare sa lista tua</string>
<string name="clear_queue_confirmation_title">Pedi una cunfirma in antis de iscantzellare una lista</string>
<string name="notification_action_shuffle">Òrdine casuale</string>
<string name="notification_actions_summary">Modìfica cada atzione de notìfica inoghe in suta incarchende·la. Ischerta·nde finas a tres de ammustrare in sa notìfica cumpata impreende sas casellas de controllu a destra.</string>
<string name="notification_actions_summary">Modìfica cada atzione de notìfica inoghe in suta incarchende·la. Ischerta·nde finas a tres de ammustrare in sa notìfica cumpata impreende sas casellas de controllu a destra</string>
<string name="notification_scale_to_square_image_summary">Sega sa miniadura ammustrada in sa notìfica dae su formadu in 16:9 a cussu 1:1</string>
<string name="notification_action_nothing">Nudda</string>
<string name="notification_action_buffering">Carrighende</string>
@@ -806,22 +806,4 @@
<string name="channel_tab_channels">Canales</string>
<string name="previous_stream">Flussu antepostu</string>
<string name="channel_tab_livestreams">Diretas</string>
<string name="notification_actions_summary_android13">Modìfica cada atzione de notìfica inoghe in suta tochende·la. Sas primas tres atziones (riprodutzione/pàusa, antepostu e imbeniente) sunt impostadas dae su sistema e non si podent personalizare.</string>
<string name="error_insufficient_storage">Non b\'at ispàtziu lìberu bastante in su dispositivu</string>
<string name="show_less">Mustra de mancu</string>
<string name="auto_update_check_description">NewPipe podet chircare in automàticu versiones noas cada tantu e notificare·ti cando sunt a disponimentu.
\nLu boles abilitare?</string>
<string name="no">Nono</string>
<string name="settings_category_backup_restore_title">Còpia de seguresa e riprìstinu</string>
<plurals name="replies">
<item quantity="one">%s risposta</item>
<item quantity="other">%s rispostas</item>
</plurals>
<string name="show_more">Mustra de prus</string>
<string name="yes">Eja</string>
<string name="reset_settings_title">Reseta sas impostatziones</string>
<string name="reset_settings_summary">Reseta totu sas impostatziones a sos valores predefinidos issoro</string>
<string name="reset_all_settings">Resetende totu sas impostatziones as a iscartare totu sas impostatziones preferidas tuas e a torrare a allùghere s\'aplicatzione.
\n
\nSes seguru de bòlere sighire?</string>
</resources>

View File

@@ -398,7 +398,7 @@
<string name="overwrite_failed">súbor nemožno prepísať</string>
<string name="download_already_pending">Súbor s rovnakým názvom už čaká na stiahnutie</string>
<string name="error_postprocessing_stopped">NewPipe bol ukončený počas spracovávania súboru</string>
<string name="error_insufficient_storage_left">V zariadení už nie je voľné miesto</string>
<string name="error_insufficient_storage_left">Máš plnú pamäť</string>
<string name="error_progress_lost">Nemožno pokračovať, súbor bol vymazaný</string>
<string name="error_timeout">Spojenie vypršalo</string>
<string name="confirm_prompt">Chcete vymazať históriu sťahovania alebo odstrániť všetky stiahnuté súbory\?</string>
@@ -827,15 +827,4 @@
<string name="video_details_list_item">- %1$s: %2$s</string>
<string name="share_playlist_content_details">%1$s
\n%2$s</string>
<string name="error_insufficient_storage">Nedostatok voľného miesta v zariadení</string>
<string name="reset_settings_summary">Obnoví všetky nastavenia na pôvodné hodnoty</string>
<string name="reset_settings_title">Obnoviť nastavenia</string>
<string name="settings_category_backup_restore_title">Záloha a obnovenie</string>
<string name="reset_all_settings">Obnovením nastavení sa zrušia všetky preferované nastavenia a aplikácia sa reštartuje.
\n
\nSte si istí, že chcete pokračovať?</string>
<string name="no">Nie</string>
<string name="yes">Áno</string>
<string name="auto_update_check_description">NewPipe môže z času na čas automaticky kontrolovať nové verzie a upozorniť vás, keď budú k dispozícii.
\nChcete to povoliť?</string>
</resources>

View File

@@ -827,15 +827,4 @@
<string name="show_more">Прикажи више</string>
<string name="show_less">Прикажи мање</string>
<string name="notification_actions_summary_android13">Измените сваку радњу обавештења у наставку тако што ћете је додирнути. Прве три радње (пусти/паузирај, претходни и следећи) поставља систем и не могу се прилагодити.</string>
<string name="error_insufficient_storage">Нема довољно слободног меморијског простора на уређају</string>
<string name="settings_category_backup_restore_title">Прављење резервне копије и враћање</string>
<string name="reset_settings_title">Ресетуј подешавања</string>
<string name="reset_settings_summary">Ресетујте сва подешавања на подразумеване вредности</string>
<string name="reset_all_settings">Ресетовање свих подешавања ће одбацити сва жељена подешавања и поново покренути апликацију.
\n
\nЖелите ли заиста да наставите?</string>
<string name="no">Не</string>
<string name="yes">Да</string>
<string name="auto_update_check_description">NewPipe може аутоматски да проверава да ли постоје нове верзије с времена на време и да вас обавести када буду доступне.
\nЖелите ли да омогућите ово?</string>
</resources>

View File

@@ -813,15 +813,4 @@
</plurals>
<string name="show_less">Visa mindre</string>
<string name="notification_actions_summary_android13">Redigera varje aviseringsåtgärd nedan genom att trycka på den. De tre första åtgärderna (spela/pausa, föregående och nästa) är satta av systemet och kan inte ändras.</string>
<string name="error_insufficient_storage">Inte tillräckligt med ledigt utrymme på enheten</string>
<string name="no">Nej</string>
<string name="settings_category_backup_restore_title">Säkerhetskopiering och återställning</string>
<string name="yes">Ja</string>
<string name="auto_update_check_description">NewPipe kan automatiskt söka efter nya versioner då och då och meddela dig när de är tillgängliga.
\nVill du aktivera detta?</string>
<string name="reset_settings_title">Återställ inställningar</string>
<string name="reset_settings_summary">Återställ alla inställningar till deras standardvärden</string>
<string name="reset_all_settings">Om du återställer alla inställningar försvinner alla dina föredragna inställningar och appen startas om.
\n
\nÄr du säker på att du vill fortsätta?</string>
</resources>

View File

@@ -474,8 +474,8 @@
<string name="feed_group_dialog_select_subscriptions">Abonelikleri seç</string>
<string name="feed_group_dialog_empty_selection">Abonelik seçilmedi</string>
<plurals name="feed_group_dialog_selection_count">
<item quantity="one">%d öge seçildi</item>
<item quantity="other">%d öge seçildi</item>
<item quantity="one">%d öğe seçildi</item>
<item quantity="other">%d öğe seçildi</item>
</plurals>
<string name="feed_group_dialog_empty_name">Boş grup adı</string>
<string name="feed_group_dialog_delete_message">Bu grubu silmek istiyor musunuz\?</string>
@@ -813,15 +813,4 @@
<item quantity="one">%s yanıt</item>
<item quantity="other">%s yanıt</item>
</plurals>
<string name="error_insufficient_storage">Aygıtta yeterli boş alan yok</string>
<string name="settings_category_backup_restore_title">Yedekle ve geri yükle</string>
<string name="reset_settings_title">Ayarları sıfırla</string>
<string name="reset_settings_summary">Tüm ayarları öntanımlı değerlerine sıfırlayın</string>
<string name="reset_all_settings">Tüm ayarları sıfırlamak, tercih edilen tüm ayarlarınızı kaldırır ve uygulamayı yeniden başlatır.
\n
\nDevam etmek istediğinizden emin misiniz?</string>
<string name="yes">Evet</string>
<string name="no">Hayır</string>
<string name="auto_update_check_description">NewPipe zaman zaman yeni sürümleri kendiliğinden denetleyebilir ve kullanılabilir olduklarında sizi bilgilendirebilir.
\nBunu etkinleştirmek istiyor musunuz?</string>
</resources>

View File

@@ -398,7 +398,7 @@
<string name="overwrite_failed">не можу перезаписати файл</string>
<string name="download_already_pending">Завантаження з такою назвою вже додано в чергу</string>
<string name="error_postprocessing_stopped">NewPipe був закритий під час роботи над файлом</string>
<string name="error_insufficient_storage_left">На пристрої не залишилося вільного простору</string>
<string name="error_insufficient_storage_left">На пристрої не залишилося вільного місця</string>
<string name="error_progress_lost">Прогрес втрачено через видалення файлу</string>
<string name="error_timeout">Час очікування з\'єднання вичерпано</string>
<string name="confirm_prompt">Очистити історію завантажень чи завантажені файли\?</string>
@@ -832,15 +832,4 @@
<item quantity="other">%s відповідей</item>
</plurals>
<string name="show_less">Показати менше</string>
<string name="error_insufficient_storage">Недостатньо вільного простору на пристрої</string>
<string name="reset_settings_title">Скинути налаштування</string>
<string name="settings_category_backup_restore_title">Резервне копіювання і відновлення</string>
<string name="auto_update_check_description">NewPipe може час від часу автоматично перевіряти наявність нових версій і сповіщати вас про їх появу.
\nХочете увімкнути цю функцію?</string>
<string name="yes">Так</string>
<string name="no">Ні</string>
<string name="reset_settings_summary">Скинути всі налаштування до усталених значень</string>
<string name="reset_all_settings">Скидання всіх налаштувань призведе до скидання всіх вибраних вами налаштувань і перезапуску застосунку.
\n
\nВи впевнені, що хочете продовжити?</string>
</resources>

View File

@@ -8,32 +8,32 @@
<string name="open_in_browser">Mở trong trình duyệt</string>
<string name="open_in_popup_mode">Mở trong chế độ bật lên</string>
<string name="share">Chia sẻ</string>
<string name="download">Tải xuống</string>
<string name="download">Tải về</string>
<string name="search">Tìm kiếm</string>
<string name="settings">Cài đặt</string>
<string name="settings">Thiết đặt</string>
<string name="did_you_mean">Ý bạn là \"%1$s\"\?</string>
<string name="share_dialog_title">Chia sẻ với</string>
<string name="use_external_video_player_title">Sử dụng trình phát video bên ngoài</string>
<string name="use_external_video_player_title">Sử dụng trình phát băng hình bên ngoài</string>
<string name="use_external_video_player_summary">Loại bỏ âm thanh ở một số độ phân giải</string>
<string name="use_external_audio_player_title">Sử dụng trình phát âm thanh bên ngoài</string>
<string name="controls_popup_title">Bật lên</string>
<string name="download_path_title">Thư mục video tải về</string>
<string name="download_path_summary">Video đã tải về được lưu ở đây</string>
<string name="download_path_dialog_title">Chọn thư mục tải xuống cho các tệp video</string>
<string name="controls_popup_title">Trình phát nổi</string>
<string name="download_path_title">Thư mục băng hình tải về</string>
<string name="download_path_summary">Băng hình đã tải về được lưu ở đây</string>
<string name="download_path_dialog_title">Chọn thư mục tải xuống cho các tệp băng hình</string>
<string name="download_path_audio_title">Thư mục tải xuống âm thanh</string>
<string name="download_path_audio_summary">Các tệp âm thanh đã tải xuống được lưu trữ tại đây</string>
<string name="download_path_audio_dialog_title">Chọn thư mục tải xuống cho các tệp âm thanh</string>
<string name="default_resolution_title">Độ phân giải mặc định</string>
<string name="default_popup_resolution_title">Độ phân giải cửa sổ bật lên mặc định</string>
<string name="show_higher_resolutions_title">Hiện độ phân giải cao hơn</string>
<string name="show_higher_resolutions_summary">Chỉ một số thiết bị có thể phát video 2K/4K</string>
<string name="show_higher_resolutions_summary">Chỉ một số thiết bị có thể phát băng hình 2K/4K</string>
<string name="play_with_kodi_title">Phát với Kodi</string>
<string name="kore_not_found">Cài đặt ứng dụng Kore bị thiếu\?</string>
<string name="show_play_with_kodi_title">Hiển thị tùy chọn \"Phát với Kodi\"</string>
<string name="show_play_with_kodi_summary">Hiển thị tùy chọn phát video qua trung tâm truyền thông Kodi</string>
<string name="show_play_with_kodi_summary">Hiển thị tùy chọn phát băng hình qua trung tâm truyền thông Kodi</string>
<string name="play_audio">Âm thanh</string>
<string name="default_audio_format_title">Định dạng âm thanh mặc định</string>
<string name="default_video_format_title">Định dạng video mặc định</string>
<string name="default_video_format_title">Định dạng băng hình mặc định</string>
<string name="controls_background_title">Nền</string>
<string name="theme_title">Chủ đề</string>
<string name="light_theme_title">Sáng</string>
@@ -43,8 +43,8 @@
<string name="popup_remember_size_pos_summary">Ghi nhớ kích thước và vị trí cuối cùng của cửa sổ bật lên</string>
<string name="show_search_suggestions_title">Đề xuất tìm kiếm</string>
<string name="show_search_suggestions_summary">Chọn các đề xuất để hiển thị khi tìm kiếm</string>
<string name="download_dialog_title">Tải xuống</string>
<string name="show_next_and_similar_title">Hiện các video \"Tiếp theo\" và \"Tương tự\"</string>
<string name="download_dialog_title">Tải về</string>
<string name="show_next_and_similar_title">Hiện các cuộn băng \"Tiếp theo\" và \"Tương tự\"</string>
<string name="unsupported_url">URL không hỗ trợ</string>
<string name="settings_category_appearance_title">Vẻ ngoài</string>
<string name="background_player_playing_toast">Đang phát trong nền</string>
@@ -57,7 +57,7 @@
<string name="error_report_title">Báo lỗi</string>
<string name="all">Tất cả</string>
<string name="disabled">Vô hiệu</string>
<string name="clear">Xóa</string>
<string name="clear">Dọn dẹp</string>
<string name="best_resolution">Độ phân giải tốt nhất</string>
<string name="general_error">Lỗi</string>
<string name="network_error">Lỗi kết nối mạng</string>
@@ -65,7 +65,7 @@
<string name="parsing_error">Không thể phân tích cú pháp trang web</string>
<string name="content_not_available">Nội dung không khả dụng</string>
<string name="could_not_setup_download_menu">Không thể thiết lập menu tải về</string>
<string name="app_ui_crash">Ứng dụng/Giao diện người dùng bị lỗi</string>
<string name="app_ui_crash">Ứng dụng / Giao diện người dùng bị lỗi</string>
<string name="sorry_string">Có vẻ NewPipe đã xảy ra lỗi, lướt xuống kiểm tra xem.</string>
<string name="error_report_button_text">Báo lỗi qua email</string>
<string name="error_snackbar_message">Rất tiếc, đã xảy ra lỗi rồi.</string>
@@ -75,11 +75,11 @@
<string name="info_labels">Loại lỗi:\\nYêu cầu:\\nNgôn ngữ của nội dung:\\nVùng miền (quốc gia) của nội dung:\\nNgôn ngữ của ứng dụng:\\nDịch vụ:\\nThời gian GMT:\\nTên gói:\\nPhiên bản:\\nPhiên bản hệ điều hành:</string>
<string name="your_comment">Nhận xét của bạn (bằng tiếng Anh):</string>
<string name="error_details_headline">Chi tiết:</string>
<string name="detail_thumbnail_view_description">Phát video, thời lượng:</string>
<string name="detail_thumbnail_view_description">Phát băng hình, thời lượng:</string>
<string name="detail_uploader_thumbnail_view_description">Hình thu nhỏ của avatar người tải lên</string>
<string name="detail_likes_img_view_description">Lượt thích</string>
<string name="detail_dislikes_img_view_description">Lượt không thích</string>
<string name="video">Video</string>
<string name="video">Băng hình</string>
<string name="audio">Âm thanh</string>
<string name="retry">Thử lại</string>
<string name="short_thousand">nghìn</string>
@@ -95,9 +95,9 @@
<string name="msg_error">Lỗi</string>
<string name="msg_running">NewPipe đang tải xuống</string>
<string name="msg_running_detail">Chạm để biết chi tiết</string>
<string name="msg_wait">Vui lòng chờ</string>
<string name="msg_wait">Đợi chút xíu nha</string>
<string name="msg_copied">Đã sao chép vào khay nhớ tạm</string>
<string name="no_available_dir">Vui lòng xác định thư mục tải xuống sau trong cài đặt</string>
<string name="no_available_dir">Hãy chọn một thư mục tải xuống trong phần thiết đặt</string>
<string name="msg_popup_permission">Sự cho phép này là cần thiết để
\nmở trong chế độ bật lên</string>
<string name="title_activity_recaptcha">reCAPTCHA</string>
@@ -105,16 +105,16 @@
<string name="title_activity_about">Giới thiệu về NewPipe</string>
<string name="title_licenses">Giấy phép của bên thứ ba</string>
<string name="copyright" formatted="true">© %1$s bởi %2$s dưới %3$s</string>
<string name="tab_about">Giới thiệu &amp; Câu hỏi thường gặp</string>
<string name="tab_about">Thông tin &amp; FAQ</string>
<string name="tab_licenses">Giấy phép</string>
<string name="app_description">Phát trực tuyến nhẹ tự do trên Android.</string>
<string name="view_on_github">Xem trên GitHub</string>
<string name="app_license_title">Giấy phép của NewPipe</string>
<string name="contribution_encouragement">Cho dù bạn có ý tưởng về: dịch thuật, thay đổi thiết kế, dọn mã hoặc thay đổi mã thực sự nhiều— sự trợ giúp luôn được hoan nghênh. Làm càng nhiều thì càng tốt!</string>
<string name="contribution_encouragement">Sự đóng góp của bạn luôn được hoan nghênh kể cả khi bạn dịch, thay đổi giao diện, dọn code, thêm tính năng hay thay đổi những thứ khác, sự giúp đỡ của bạn vẫn đáng được trân trọng. Bạn càng làm nhiều, ứng dụng này sẽ càng tốt hơn bao giờ hết !</string>
<string name="read_full_license">Đọc giấy phép</string>
<string name="contribution_title">Đóng góp</string>
<string name="content_language_title">Ngôn ngữ nội dung ưu tiên</string>
<string name="settings_category_video_audio_title">Video và âm thanh</string>
<string name="settings_category_video_audio_title">Băng hình và âm thanh</string>
<string name="enable_watch_history_title">Lịch sử xem</string>
<string name="settings_category_history_title">Lịch sử và bộ nhớ đệm</string>
<string name="search_no_results">Không tìm thấy</string>
@@ -133,18 +133,18 @@
<string name="use_inexact_seek_title">Dùng tua nhanh ít chính xác</string>
<string name="use_inexact_seek_summary">Tua ít chính xác cho phép trình phát giảm độ chính xác để tua tới vị trí nhanh hơn. Tua khoảng 5, 15 hoặc 25 giây không hoạt động với điều này</string>
<string name="thumbnail_cache_wipe_complete_notice">Đã xóa bộ nhớ cache hình ảnh</string>
<string name="metadata_cache_wipe_title">Xóa sạch siêu dữ liệu đã lưu đệm</string>
<string name="metadata_cache_wipe_summary">Xóa tất cả dữ liệu trang web được lưu trong bộ nhớ cache</string>
<string name="metadata_cache_wipe_title">Lau sạch siêu dữ liệu đã lưu đệm</string>
<string name="metadata_cache_wipe_summary">Loại bỏ mọi dữ liệu trang web đã lưu đệm</string>
<string name="metadata_cache_wipe_complete_notice">Đã xóa bộ nhớ cache siêu dữ liệu</string>
<string name="auto_queue_title">Tự động xếp hàng luồng phát tiếp theo</string>
<string name="auto_queue_summary">Tiếp tục hàng đợi (không lặp lại) bằng cách thêm một luồng phát liên quan</string>
<string name="enable_search_history_title">Lịch sử tìm kiếm</string>
<string name="enable_search_history_summary">Lưu trữ truy vấn tìm kiếm cục bộ</string>
<string name="enable_watch_history_summary">Theo dõi các video đã xem</string>
<string name="enable_watch_history_summary">Theo dõi các cuộn băng đã xem</string>
<string name="resume_on_audio_focus_gain_title">Tiếp tục đang phát</string>
<string name="resume_on_audio_focus_gain_summary">Tiếp tục phát lại sau khi bị gián đoạn (ví dụ: cuộc gọi)</string>
<string name="show_hold_to_append_title">Hiển thị mẹo \"Giữ để xếp hàng\"</string>
<string name="show_hold_to_append_summary">Hiển thị mẹo khi nhấn vào nút nền hoặc nút bật lên trong \"Chi tiết:\" video</string>
<string name="show_hold_to_append_summary">Hiển thị mẹo khi nhấn vào nút nền hoặc nút bật lên trong \"Chi tiết:\" cuốn băng</string>
<string name="default_content_country_title">Quốc gia nội dung mặc định</string>
<string name="settings_category_player_title">Trình phát</string>
<string name="settings_category_player_behavior_title">Hành vi</string>
@@ -163,16 +163,16 @@
<string name="unknown_content">[Không xác định]</string>
<string name="switch_to_background">Chuyển sang nền</string>
<string name="switch_to_popup">Chuyển sang Cửa sổ bật lên</string>
<string name="switch_to_main">Chuyển sang Chính</string>
<string name="switch_to_main">Chuyển sang Main</string>
<string name="import_data_title">Nhập cơ sở dữ liệu</string>
<string name="export_data_title">Xuất cơ sở dữ liệu</string>
<string name="import_data_summary">Ghi đè lịch sử, đăng ký, danh sách phát và các cài đặt (tùy chọn) hiện tại của bạn</string>
<string name="export_data_summary">Xuất lịch sử, đăng ký, danh sách phát và các cài đặt</string>
<string name="clear_views_history_title">Xóa lịch sử xem</string>
<string name="import_data_summary">Ghi đè lịch sử, đăng ký, danh sách phát và các thiết đặt (tùy chọn) hiện tại của bạn</string>
<string name="export_data_summary">Xuất lịch sử, đăng ký, danh sách phát và các thiết đặt</string>
<string name="clear_views_history_title">Dọn dẹp lịch sử xem</string>
<string name="clear_views_history_summary">Xóa lịch sử các luồng đã phát và các vị trí phát lại</string>
<string name="delete_view_history_alert">Xóa toàn bộ lịch sử xem\?</string>
<string name="watch_history_deleted">Đã xoá lịch sử xem</string>
<string name="clear_search_history_title">Xóa lịch sử tìm kiếm</string>
<string name="clear_search_history_title">Dọn dẹp lịch sử tìm kiếm</string>
<string name="clear_search_history_summary">Xóa lịch sử tìm kiếm mà bạn đã ghi</string>
<string name="delete_search_history_alert">Xóa toàn bộ lịch sử tìm kiếm\?</string>
<string name="search_history_deleted">Đã xóa lịch sử tìm kiếm</string>
@@ -180,11 +180,11 @@
<string name="player_unrecoverable_failure">Đã xảy ra lỗi trình phát không thể khôi phục</string>
<string name="player_recoverable_failure">Phục hồi lại trình phát bị lỗi</string>
<string name="external_player_unsupported_link_type">Trình phát ngoài không hỗ trợ các loại liên kết này</string>
<string name="video_streams_empty">Không tìm thấy luồng video nào</string>
<string name="video_streams_empty">Không tìm thấy luồng băng hình nào</string>
<string name="audio_streams_empty">Không tìm thấy luồng âm thanh nào</string>
<string name="invalid_directory">Thư mục không hợp lệ</string>
<string name="invalid_source">Tệp/nguồn nội dung không hợp lệ</string>
<string name="invalid_file">Tệp không tồn tại hoặc không có quyền đọc/ghi</string>
<string name="invalid_source">Tệp / nguồn nội dung không hợp lệ</string>
<string name="invalid_file">Tệp không tồn tại hoặc không có quyền đọc / ghi</string>
<string name="file_name_empty_error">Tên tệp không được để trống</string>
<string name="error_occurred_detail">Đã xảy ra lỗi: %1$s</string>
<string name="no_streams_available_download">Không có luồng nào để tải về</string>
@@ -192,17 +192,17 @@
<string name="detail_drag_description">Kéo để sắp xếp lại</string>
<string name="no_subscribers">Không có người đăng ký</string>
<plurals name="subscribers">
<item quantity="other">%s người đăng ký</item>
<item quantity="other">%s người đăng kí</item>
</plurals>
<string name="no_views">Không có lượt xem nào</string>
<plurals name="views">
<item quantity="other">%s lượt xem</item>
</plurals>
<string name="no_videos">Không có video nào</string>
<string name="no_videos">Không có cuộn băng nào</string>
<plurals name="videos">
<item quantity="other">%s video</item>
<item quantity="other">%s cuộn băng</item>
</plurals>
<string name="create">Tạo</string>
<string name="create">Tạo nên</string>
<string name="dismiss">Bỏ qua</string>
<string name="rename">Đổi tên</string>
<string name="one_item_deleted">Đã xóa 1 mục.</string>
@@ -212,9 +212,9 @@
<string name="settings_file_replacement_character_title">Ký tự thay thế</string>
<string name="charset_letters_and_digits">Chỉ chữ cái và chữ số</string>
<string name="charset_most_special_characters">Hầu hết các ký tự đặc biệt</string>
<string name="donation_title">Quyên tặng</string>
<string name="donation_encouragement">NewPipe được phát triển bởi các tình nguyện viên dành thời gian rảnh rỗi để mang lại cho bạn trải nghiệm người dùng tốt nhất. Hãy đền đáp để giúp các nhà phát triển làm cho NewPipe thậm chí còn tốt hơn nữa trong khi họ thưởng thức một tách cà phê.</string>
<string name="give_back">Đn đáp</string>
<string name="donation_title">Đóng góp</string>
<string name="donation_encouragement">NewPipe được phát triển bởi các tình nguyện viên dành thời gian và tâm huyết của mình để mang lại cho bạn trải nghiệm tốt nhất. Đóng góp một chút xiền để giúp chúng tôi làm NewPipe tốt hơn nữa (Nếu bạn muốn).</string>
<string name="give_back">Đôn Nét</string>
<string name="website_title">Trang web</string>
<string name="website_encouragement">Truy cập website chính thức của NewPipe để biết thêm thông tin và tin tức.</string>
<string name="privacy_policy_title">Chính sách bảo mật của NewPipe</string>
@@ -239,12 +239,12 @@
<string name="no_valid_zip_file">Không có tệp ZIP hợp lệ</string>
<string name="could_not_import_all_files">Cảnh báo: Không thể nhập tất cả các tệp.</string>
<string name="override_current_data">Thao tác này sẽ ghi đè cài đặt hiện tại của bạn.</string>
<string name="import_settings">Bạn có muốn cũng nhập các cài đặt không?</string>
<string name="import_settings">Bạn có muốn cũng nhập các thiết đặt không\?</string>
<string name="trending">Thịnh hành</string>
<string name="new_and_hot">Mới và nóng</string>
<string name="new_and_hot">Mới và đang hot</string>
<string name="play_queue_remove">Loại bỏ</string>
<string name="play_queue_stream_detail">Chi tiết</string>
<string name="play_queue_audio_settings">Cài đặt âm thanh</string>
<string name="play_queue_audio_settings">Thiết đặt âm thanh</string>
<string name="hold_to_append">Giữ để xếp hàng</string>
<string name="start_here_on_background">Bắt đầu phát từ đây trong nền</string>
<string name="start_here_on_popup">Bắt đầu phát trong cửa sổ bật lên</string>
@@ -252,7 +252,7 @@
<string name="drawer_close">Đóng ngăn</string>
<string name="preferred_open_action_settings_title">Hành động \'mở\' được ưu tiên</string>
<string name="preferred_open_action_settings_summary">Hành động mặc định khi mở nội dung — %s</string>
<string name="video_player">Trình phát video</string>
<string name="video_player">Trình phát băng hình</string>
<string name="background_player">Trình phát nền</string>
<string name="popup_player">Trình phát bật lên</string>
<string name="always_ask_open_action">Luôn luôn hỏi</string>
@@ -276,14 +276,14 @@
<string name="caption_auto_generated">Tự động tạo ra</string>
<string name="caption_setting_title">Phụ đề</string>
<string name="caption_setting_description">Thay đổi tỷ lệ văn bản và kiểu nền phụ đề trình phát. Yêu cầu khởi động lại ứng dụng để có hiệu lực</string>
<string name="enable_leak_canary_summary">Giám sát rò rỉ bộ nhớ có thể khiến ứng dụng không phản hồi khi tải vùng lưu trữ</string>
<string name="enable_disposed_exceptions_title">Báo cáo lỗi ngoài vòng đời</string>
<string name="enable_leak_canary_summary">Theo dõi rò rỉ bộ nhớ có thể khiến ứng dụng trở nên không phản hồi khi đổ xô đống</string>
<string name="enable_disposed_exceptions_title">Báo các lỗi out-of-lifecycle</string>
<string name="enable_disposed_exceptions_summary">Buộc báo cáo ngoại lệ Rx không thể gửi được bên ngoài vòng đời của mảnh hoặc hoạt động sau khi xử lý</string>
<string name="import_title">Nhập</string>
<string name="import_from">Nhập từ</string>
<string name="export_to">Xuất sang</string>
<string name="import_ongoing">Đang nhập…</string>
<string name="export_ongoing">Đang xuất…</string>
<string name="export_ongoing">Đang xuất </string>
<string name="import_file_title">Nhập tệp</string>
<string name="previous_export">Xuất trước</string>
<string name="subscriptions_import_unsuccessful">Không thể nhập đăng ký</string>
@@ -292,10 +292,10 @@
\n
\n1. Vào URL này: %1$s
\n2. Đăng nhập khi được yêu cầu
\n3. Nhấn chọn \"Bao gồm tất cả dữ liệu trên YouTube\", sau đó nhấn \"Bỏ chọn tất cả\", sau đó chỉ chọn mục \"đăng ký\" rồi nhấn OK
\n3. Nhấn chọn \"Bao gồm tất cả dữ liệu trên YouTube\", sau đó nhấn \"Bỏ chọn tất cả\", sau đó chỉ chọn mục \"đăng kí\" rồi nhấn OK
\n4. Nhấn nút \"Bước tiếp theo\" rồi nhấn \"Tạo tệp xuất\"
\n5. Nhấn nút \"Tải xuống\" khi nó xuất hiện
\n6. Từ file zip mới tải về, trích xuất file .json ra (thường nằm ở đường dẫn \"YouTube và YouTube Music/đăng ký/subscriptions.json\") rồi nhập vào đây.
\n6. Từ file zip mới tải về, trích xuất file .json ra (thường nằm ở đường dẫn \"YouTube và YouTube Music/đăng kí/subscriptions.json\") rồi nhập vào đây.
\n7. [Nếu nhập file .zip không thành công] Hãy giải nén tệp .csv (thường nó được để dưới phần \"YouTube and YouTube Music/subscriptions/subscriptions.csv\"), nhấn vào nút NHẬP TỆP ở phía bên dưới rồi chọn tệp csv đã được giải nén</string>
<string name="import_soundcloud_instructions">Để nhập hồ sơ SoundCloud bằng cách nhập URL hoặc ID của bạn, hãy làm các bước như sau:
\n
@@ -309,8 +309,8 @@
<string name="playback_speed_control">Điều khiển tốc độ phát lại</string>
<string name="playback_tempo">Tốc độ</string>
<string name="playback_pitch">Độ cao</string>
<string name="unhook_checkbox">Bỏ gắn (có thể gây méo)</string>
<string name="skip_silence_checkbox">Chuyển nhanh qua khoảng lặng</string>
<string name="unhook_checkbox">Bỏ gắn (có thể gây méo nhưng vui)</string>
<string name="skip_silence_checkbox">Tua nhanh trong im lặng</string>
<string name="playback_step">Bước</string>
<string name="playback_reset">Đặt lại</string>
<string name="start_accept_privacy_policy">Để tuân thủ Quy định bảo vệ dữ liệu chung của châu Âu (GDPR), chúng tôi sẽ thu hút sự chú ý của bạn đến chính sách bảo mật của NewPipe. Vui lòng đọc kỹ.
@@ -318,9 +318,9 @@
<string name="accept">Chấp nhận</string>
<string name="decline">Từ chối</string>
<string name="limit_data_usage_none_description">Không giới hạn</string>
<string name="limit_mobile_data_usage_title">Giới hạn độ phân giải khi sử dụng dữ liệu di động</string>
<string name="limit_mobile_data_usage_title">Giới hạn độ phân giải khi sử dụng 3G, 4G</string>
<string name="minimize_on_exit_title">Thu nhỏ khi chuyển qua ứng dụng khác</string>
<string name="minimize_on_exit_summary">Hành động khi chuyển sang ứng dụng khác từ trình phát video chính — %s</string>
<string name="minimize_on_exit_summary">Hành động khi chuyển sang ứng dụng khác từ trình phát băng hình chính — %s</string>
<string name="minimize_on_exit_none_description">Không</string>
<string name="minimize_on_exit_background_description">Thu nhỏ xuống trình phát nền</string>
<string name="minimize_on_exit_popup_description">Thu nhỏ xuống trình phát bật lên</string>
@@ -353,7 +353,7 @@
<string name="post_processing">đang xử lý</string>
<string name="enqueue">Xếp hàng</string>
<string name="permission_denied">Thao tác bị từ chối bởi hệ thống</string>
<string name="download_failed">Tải xuống thất bại</string>
<string name="download_failed">Tải về không thành công</string>
<string name="generate_unique_name">Tạo tên riêng biệt</string>
<string name="overwrite">Ghi đè</string>
<string name="overwrite_finished_warning">Có một tệp đã tải về trùng tên</string>
@@ -385,13 +385,13 @@
<string name="enable_playback_resume_summary">Khôi phục vị trí phát lại cuối cùng</string>
<string name="enable_playback_state_lists_title">Vị trí phát trong danh sách</string>
<string name="enable_playback_state_lists_summary">Hiển chỉ báo vị trí phát lại trong danh sách</string>
<string name="settings_category_clear_data_title">Xóa dữ liệu</string>
<string name="settings_category_clear_data_title">Dọn dẹp dữ liệu</string>
<string name="watch_history_states_deleted">Đã xoá vị trí phát</string>
<string name="missing_file">Tệp đã di chuyển hoặc đã xoá</string>
<string name="overwrite_unrelated_warning">Tên file này đã tồn tại</string>
<string name="overwrite_failed">Không thể ghi đè lên tệp</string>
<string name="download_already_pending">Có một bản tải xuống đang chờ xử lý với tên này</string>
<string name="error_postprocessing_stopped">NewPipe đã bị đóng khi đang xử lý tệp</string>
<string name="download_already_pending">Có một bản tải xuống đang chờ xử lí với tên này</string>
<string name="error_postprocessing_stopped">Newpipe đã bị đóng khi đang xử lí tệp</string>
<string name="error_insufficient_storage_left">Không đủ dung lượng trên máy</string>
<string name="error_progress_lost">Quá trình tải bị hủy, vì tập tin đã bị xoá</string>
<string name="error_timeout">Kết nối hết thời gian</string>
@@ -425,11 +425,11 @@
<plurals name="seconds">
<item quantity="other">%d giây</item>
</plurals>
<string name="remove_watched_popup_yes_and_partially_watched_videos">Có, và video đã xem một phần</string>
<string name="remove_watched_popup_warning">Những video đã xem trước và sau khi thêm vào danh sách phát sẽ bị loại bỏ.
\nBạn có chắc không? Điều này không thể được hoàn tác!</string>
<string name="remove_watched_popup_title">Xóa các video đã xem?</string>
<string name="remove_watched">Loại bỏ đã xem</string>
<string name="remove_watched_popup_yes_and_partially_watched_videos">Có, và cuốn băng đã xem một phần</string>
<string name="remove_watched_popup_warning">Những cuốn băng đã xem trước và sau khi thêm vào danh sách phát sẽ bị loại bỏ.
\nBạn có chắc không\? Điều này không thể được hoàn tác!</string>
<string name="remove_watched_popup_title">Loại bỏ các cuốn băng đã xem\?</string>
<string name="remove_watched">Xóa đã xem</string>
<string name="systems_language">Mặc định hệ thống</string>
<string name="app_language_title">Ngôn ngữ ứng dụng</string>
<string name="downloads_storage_use_saf_summary">\'Khung truy cập lưu trữ\' cho phép tải xuống một thẻ SD bên ngoài</string>
@@ -444,8 +444,8 @@
<string name="localization_changes_requires_app_restart">Ngôn ngữ sẽ thay đổi khi ứng dụng khởi động lại</string>
<string name="subtitle_activity_recaptcha">Bấm \"Xong\" khi hoàn thành</string>
<string name="done">Đã hoàn thành</string>
<string name="infinite_videos">video</string>
<string name="more_than_100_videos">100+ video</string>
<string name="infinite_videos">cuộn băng</string>
<string name="more_than_100_videos">100+ cuốn băng</string>
<plurals name="listening">
<item quantity="other">%s người nghe</item>
</plurals>
@@ -458,17 +458,17 @@
<string name="artists">Nghệ sĩ</string>
<string name="albums">Album</string>
<string name="songs">Bài hát</string>
<string name="videos_string">Các video</string>
<string name="restricted_video">Video này bị giới hạn độ tuổi.
<string name="videos_string">Các cuốn băng</string>
<string name="restricted_video">Cuốn băng này bị giới hạn độ tuổi.
\n
\nBật \"%1$s\" trong cài đặt nếu bạn muốn xem video này.</string>
<string name="youtube_restricted_mode_enabled_title">Bật chế độ hạn chế YouTube</string>
\nBật \"%1$s\" trong thiết đặt nếu bạn muốn xem cuốn băng này.</string>
<string name="youtube_restricted_mode_enabled_title">Bật chế độ hạn chế Youtube</string>
<string name="peertube_instance_add_https_only">Chỉ URL HTTPS được hỗ trợ</string>
<string name="peertube_instance_url_summary">Chọn thực thể PeerTube ưa thích</string>
<string name="peertube_instance_url_title">Thực thể PeerTube</string>
<string name="seek_duration_title">Thời lượng tua-nhanh tới/-lùi</string>
<string name="show_original_time_ago_summary">Dòng chữ mô tả thời gian gốc từ các dịch vụ sẽ được hiển thị thay thế</string>
<string name="show_original_time_ago_title">Hiển thị thời gian ban đầu trước đây trên các mục</string>
<string name="show_original_time_ago_title">Hiện thời gian gốc trên các item</string>
<string name="drawer_header_description">Chọn dịch vụ; dịch vụ hiện tại:</string>
<string name="video_detail_by">Bởi %s</string>
<string name="channel_created_by">Được tạo bởi %s</string>
@@ -476,13 +476,13 @@
<string name="content_not_supported">NewPipe chưa hỗ trợ loại nội dung này.
\n
\nCó thể nó sẽ được hỗ trợ bởi một phiên bản mới hơn trong tương lai.</string>
<string name="feed_use_dedicated_fetch_method_help_text">Bạn có nghĩ rằng tải nguồn cấp dữ liệu quá chậm? Nếu vậy, hãy thử bật tải nhanh (bạn có thể thay đổi nó này trong cài đặt hoặc bằng cách nhấn nút bên dưới).
<string name="feed_use_dedicated_fetch_method_help_text">Bạn có nghĩ rằng tải nguồn cấp dữ liệu quá chậm\? Nếu vậy, hãy thử bật tải nhanh (bạn có thể thay đổi nó này trong thiết đặt hoặc bằng cách nhấn nút bên dưới).
\n
\nNewPipe cung cấp hai chiến lược tải nguồn cấp dữ liệu:
\n• Tìm nạp toàn bộ kênh đăng ký, tuy chậm nhưng đầy đủ.
\n• Sử dụng điểm cuối dịch vụ chuyên dụng, nhanh nhưng thường không hoàn thiện.
\n
\nSự khác biệt giữa hai loại này là cái nào nhanh thường thiếu một số thông tin, chẳng hạn như thời lượng hoặc loại mục (không thể phân biệt giữa các video trực tiếp và bình thường) và nó có thể trả về ít mục hơn.
\nSự khác biệt giữa hai loại này là cái nào nhanh thường thiếu một số thông tin, chẳng hạn như thời lượng hoặc loại mục (không thể phân biệt giữa các cuốn băng trực tiếp và bình thường) và nó có thể trả về ít mục hơn.
\n
\nYouTube là một ví dụ về dịch vụ cung cấp phương pháp nhanh này với nguồn cấp RSS.
\n
@@ -523,7 +523,7 @@
<string name="error_report_open_github_notice">Vui lòng kiểm tra xem vấn đề mà bạn đang gặp đã báo cáo trước đó hay chưa. Nếu bạn tạo quá nhiều báo cáo trùng lặp, bạn sẽ khiến cho chúng tôi tốn thời gian để đọc chúng thay vì sửa lỗi bạn gặp.</string>
<string name="error_report_open_issue_button_text">Báo cáo trên GitHub</string>
<string name="copy_for_github">Sao chép bản báo cáo đã được định dạng</string>
<string name="unsupported_url_dialog_message">Không thể đọc URL này. Mở với ứng dụng khác?</string>
<string name="unsupported_url_dialog_message">Không thể đọc URL này. Mở với app khác\?</string>
<string name="auto_queue_toggle">Tự động xếp hàng</string>
<string name="clear_queue_confirmation_description">Hàng đợi của trình phát hiện tại sẽ bị thay thế</string>
<string name="clear_queue_confirmation_summary">Việc chuyển từ trình phát này sang trình phát khác có thể sẽ thay thế hàng đợi</string>
@@ -539,7 +539,7 @@
<string name="notification_action_2_title">Nút hành động thứ ba</string>
<string name="notification_action_1_title">Nút hành động thứ hai</string>
<string name="notification_action_0_title">Nút hành động đầu tiên</string>
<string name="notification_scale_to_square_image_summary">Cắt bớt hình thu nhỏ video hiển thị trong thông báo từ tỷ lệ khung hình 16:9 xuống 1:1</string>
<string name="notification_scale_to_square_image_summary">Cắt bớt hình thu nhỏ cuộn băng hiển thị trong thông báo từ tỷ lệ khung hình 16:9 xuống 1:1</string>
<string name="notification_scale_to_square_image_title">Chỉnh ảnh thu nhỏ thành tỉ lệ 1:1</string>
<string name="search_showing_result_for">Đang hiện kết quả cho: %s</string>
<string name="enqueue_stream">Xếp hàng</string>
@@ -550,7 +550,7 @@
<string name="enqueued">Đã xếp hàng</string>
<string name="clear_cookie_summary">Xoá Cookie mà NewPipe lưu trữ sau khi bạn hoàn thành nó</string>
<string name="recaptcha_cookies_cleared">Cookie reCAPTCHA đã được xóa</string>
<string name="clear_cookie_title">Xóa Cookie của reCAPCHA</string>
<string name="clear_cookie_title">Dọn dẹp Cookie của reCAPCHA</string>
<string name="youtube_restricted_mode_enabled_summary">YouTube cung cấp \"Chế độ hạn chế\" để ẩn nội dung có khả năng dành cho người trưởng thành</string>
<string name="notification_colorize_summary">Yêu cầu Android tùy chỉnh màu của thông báo theo màu chính của ảnh thu nhỏ (lưu ý rằng việc này không khả dụng trên tất cả thiết bị)</string>
<string name="notification_colorize_title">Tô màu thông báo</string>
@@ -561,11 +561,11 @@
<string name="description_tab_description">Mô tả</string>
<string name="related_items_tab_description">Các mục liên quan</string>
<string name="comments_tab_description">Bình luận</string>
<string name="hash_channel_description">Thông báo cho quá trình băm video</string>
<string name="hash_channel_name">Thông báo băm video</string>
<string name="show_meta_info_summary">Tắt để ẩn các hộp siêu dữ liệu có thông tin bổ sung về người tạo luồng, nội dung luồng hoặc yêu cầu tìm kiếm</string>
<string name="show_meta_info_title">Hiển thị thông tin meta</string>
<string name="show_description_summary">Tắt để ẩn mô tả video và các thông tin bổ sung</string>
<string name="hash_channel_description">Thông báo cho quá trình băm cuốn băng</string>
<string name="hash_channel_name">Thông báo băm cuộn băng</string>
<string name="show_meta_info_summary">Tắt để ẩn các hộp siêu thông tin có thông tin bổ sung về người tạo luồng, nội dung luồng hoặc yêu cầu tìm kiếm</string>
<string name="show_meta_info_title">Hiển thị siêu thông tin</string>
<string name="show_description_summary">Tắt để ẩn mô tả cuộn băng và các thông tin bổ sung</string>
<string name="show_description_title">Hiện mô tả</string>
<string name="open_with">Mở bằng</string>
<string name="download_has_started">Tệp đang được tải xuống</string>
@@ -574,18 +574,18 @@
<string name="auto_device_theme_title">Tự động (giao diện hệ thống)</string>
<string name="radio">Radio</string>
<string name="paid_content">Nội dung này chỉ dành cho người dùng trả phí, nên NewPipe không thể phát hay tải xuống.</string>
<string name="youtube_music_premium_content">Video này chỉ được dành cho thành viên YouTube Music Premium, nên NewPipe không thể phát hay tải xuống.</string>
<string name="youtube_music_premium_content">Cuốn băng này chỉ được dành cho thành viên YouTube Music Premium, nên NewPipe không thể phát hay tải xuống.</string>
<string name="private_content">Nội dung này được để ở chế độ riêng tư, nên NewPipe không thể phát hay tải xuống.</string>
<string name="soundcloud_go_plus_content">Đây là bản nhạc SoundCloud Go+, ít nhất là ở quốc gia của bạn, vì vậy NewPipe không thể phát trực tuyến hoặc tải xuống bản nhạc này.</string>
<string name="soundcloud_go_plus_content">Đây là một track SoundCloud Go+, nên NewPipe không thể phát hay tải xuống được, ít nhất là tại quốc gia của bạn.</string>
<string name="georestricted_content">Nội dung này không có sẵn ở quốc gia của bạn.</string>
<string name="crash_the_app">Làm văng ứng dụng</string>
<string name="restricted_video_no_stream">Video này bị giới hạn độ tuổi.
\nDo chính sách mới của YouTube với các video bị giới hạn độ tuổi, NewPipe không thể truy cập bất kỳ luồng video nào của nó và do đó không thể phát nó.</string>
<string name="restricted_video_no_stream">Cuốn băng này bị giới hạn độ tuổi.
\nDo chính sách mới của YouTube với các cuốn băng bị giới hạn độ tuổi, NewPipe không thể truy cập bất kỳ luồng băng hình nào của nó và do đó không thể phát nó.</string>
<string name="night_theme_title">Chủ đề đêm</string>
<string name="featured">Nổi bật</string>
<string name="show_channel_details">Hiện chi tiết kênh</string>
<string name="recaptcha_solve">Hoàn thành</string>
<string name="disable_media_tunneling_summary">Tắt tính năng truyền tải phương tiện nếu bạn gặp phải tình trạng màn hình đen hoặc giật hình khi phát lại video.</string>
<string name="disable_media_tunneling_summary">Tắt tính năng truyền tải phương tiện nếu bạn gặp phải tình trạng màn hình đen hoặc giật hình khi phát lại cuốn băng.</string>
<string name="disable_media_tunneling_title">Tắt truyền phương tiện qua đường hầm</string>
<string name="off">Tắt</string>
<string name="on">Bật</string>
@@ -614,7 +614,7 @@
\nBạn có muốn hủy đăng ký khỏi kênh này không\?</string>
<string name="feed_load_error_account_info">Không thể tải nguồn cấp dữ liệu cho \'%s\'.</string>
<string name="feed_load_error">Lỗi tải nguồn cấp dữ liệu</string>
<string name="downloads_storage_use_saf_summary_api_29">\'Khung truy cập lưu trữ\' chỉ được hỗ trợ từ Android 10 trở lên</string>
<string name="downloads_storage_use_saf_summary_api_29">\'Storage Access Framework\' chỉ được hỗ trợ từ Android 10 trở đi</string>
<string name="downloads_storage_ask_summary_no_saf_notice">Bạn sẽ được hỏi nơi bạn muốn lưu mỗi mục tải xuống</string>
<string name="no_dir_yet">Chưa có thư mục tải xuống nào được đặt, hãy chọn thư mục tải xuống mặc định ngay</string>
<string name="dont_show">Không hiện</string>
@@ -624,7 +624,7 @@
<string name="comments_are_disabled">Bình luận đã bị tắt</string>
<string name="detail_heart_img_view_description">Đã được chủ kênh thả \"thính\"</string>
<string name="mark_as_watched">Đánh dấu là đã xem</string>
<string name="show_image_indicators_summary">Hin thị các dải băng màu Picasso trên đầu các hình ảnh cho biết nguồn của chúng: màu đỏ cho mạng, màu lam cho đĩa và màu lục cho bộ nhớ</string>
<string name="show_image_indicators_summary">Hin ruy băng được tô màu Picasso trên cùng các hình ảnh và chỉ ra nguồn của chúng: đỏ đối với mạng, xanh lam đối với ổ đĩa và xanh lá đối với bộ nhớ</string>
<string name="show_image_indicators_title">Hiện dấu chỉ hình ảnh</string>
<string name="remote_search_suggestions">Đề xuất tìm kiếm trên mạng</string>
<string name="local_search_suggestions">Đề xuất tìm kiếm cục bộ</string>
@@ -634,12 +634,12 @@
<plurals name="download_finished_notification">
<item quantity="other">%s lượt tải xuống đã hoàn tất</item>
</plurals>
<string name="main_page_content_swipe_remove">Vuốt các mục để loại bỏ chúng</string>
<string name="start_main_player_fullscreen_summary">Không bắt đầu các video ở trình phát mini, mà chuyển trực tiếp thành chế độ toàn màn hình, nếu tự động xoay bị khóa. Bạn vẫn có thể truy cập trình phát mini bằng cách thoát khỏi toàn màn hình</string>
<string name="main_page_content_swipe_remove">Vuốt các mục để xóa chúng</string>
<string name="start_main_player_fullscreen_summary">Không bắt đầu các cuốn băng ở trình phát mini, mà chuyển trực tiếp thành chế độ toàn màn hình, nếu tự động xoay bị khóa. Bạn vẫn có thể truy cập trình phát mini bằng cách thoát khỏi toàn màn hình</string>
<string name="start_main_player_fullscreen_title">Khởi động trình phát chính ở toàn màn hình</string>
<string name="enqueued_next">Đã xếp kế tiếp vào hàng</string>
<string name="enqueue_next_stream">Xếp kế tiếp vào hàng</string>
<string name="processing_may_take_a_moment">Đang xử lý... Có thể mất chút thời gian</string>
<string name="processing_may_take_a_moment">Đang xử lý... Có thể hơi lâu</string>
<string name="error_report_channel_name">Thông báo lỗi</string>
<string name="error_report_channel_description">Thông báo để báo cáo lỗi</string>
<string name="error_report_notification_title">NewPipe đã gặp sự cố, nhấn để xem và báo cáo</string>
@@ -683,12 +683,12 @@
<string name="percent">Phần trăm</string>
<string name="enumeration_comma">,</string>
<string name="semitone">Nửa cung</string>
<string name="streams_not_yet_supported_removed">Các luồng chưa được trình tải xuống hỗ trợ sẽ không được hiển thị</string>
<string name="no_video_streams_available_for_external_players">Không có luồng video nào khả dụng cho trình phát bên ngoài</string>
<string name="streams_not_yet_supported_removed">Luồng băng hình mà không được trình tải xuống hỗ trợ sẽ không hiển thị</string>
<string name="no_video_streams_available_for_external_players">Không có luồng băng hình nào khả dụng cho trình phát bên ngoài</string>
<string name="selected_stream_external_player_not_supported">Luồng phát đã chọn không được trình phát ngoài hỗ trợ</string>
<string name="no_audio_streams_available_for_external_players">Không có luồng âm thanh nào khả dụng cho máy phát bên ngoài</string>
<string name="select_quality_external_players">Chọn chất lượng cho trình chạy ngoài</string>
<string name="unknown_format">Định dạng không xác định</string>
<string name="unknown_format">Định dạng không xác định (:P)</string>
<string name="unknown_quality">Độ phân giải không xác định</string>
<string name="progressive_load_interval_title">Kích thước tải thời lượng phát lại</string>
<string name="sort">Thể loại</string>
@@ -705,9 +705,9 @@
<string name="fast_mode">Chế độ tăng tốc</string>
<string name="duplicate_in_playlist">Danh sách phát màu xám thì đã chứa mục này.</string>
<string name="ignore_hardware_media_buttons_summary">Hữu ích trong trường hợp phím bấm âm lượng trên tai nghe hoặc thiết bị của bạn bị hỏng</string>
<string name="ignore_hardware_media_buttons_title">Bỏ qua nhận nút phương tiện vật lý</string>
<string name="remove_duplicates">Loại bỏ các bản trùng lặp</string>
<string name="remove_duplicates_title">Loại bỏ các bản trùng lặp?</string>
<string name="ignore_hardware_media_buttons_title">Không nhận phím điều khiển âm lượng vật lý</string>
<string name="remove_duplicates">Loại bỏ mục trùng lặp</string>
<string name="remove_duplicates_title">Loại bỏ mục trùng lặp\?</string>
<string name="remove_duplicates_message">Bạn có muốn loại bỏ mọi luồng trùng lặp trong danh sách phát này\?</string>
<string name="feed_show_hide_streams">Hiện/Ẩn luồng phát</string>
<string name="feed_hide_streams_title">Hiển thị các luồng phát sau</string>
@@ -717,9 +717,9 @@
<string name="progressive_load_interval_summary">Thay đổi kích thước khoảng thời gian tải trên nội dung lũy tiến (hiện tại là %s). Giá trị thấp hơn có thể tăng tốc độ tải ban đầu của chúng</string>
<string name="audio_track_present_in_video">Một bản âm thanh đã có sẵn trong luồng này</string>
<string name="use_exoplayer_decoder_fallback_title">Sử dụng tính năng bộ giải mã dự phòng của ExoPlayer</string>
<string name="always_use_exoplayer_set_output_surface_workaround_summary">Giải pháp thay thế này giải phóng và khởi tạo lại codec video khi xảy ra thay đổi bề mặt, thay vì cài đặt trực tiếp bề mặt vào codec. Đã được ExoPlayer sử dụng trên một số thiết bị gặp sự cố này, cài đặt này chỉ ảnh hưởng đến Android 6 trở lên
<string name="always_use_exoplayer_set_output_surface_workaround_summary">Giải pháp thay thế này giải phóng và khởi tạo lại codec băng hình khi xảy ra thay đổi bề mặt, thay vì thiết đặt trực tiếp bề mặt vào codec. Đã được ExoPlayer sử dụng trên một số thiết bị gặp sự cố này, thiết đặt này chỉ ảnh hưởng đến Android 6 trở lên
\n
\nBật tùy chọn này có thể ngăn lỗi phát lại khi chuyển đổi trình phát video hiện tại hoặc chuyển sang chế độ toàn màn hình</string>
\nBật tùy chọn này có thể ngăn lỗi phát lại khi chuyển đổi trình phát băng hình hiện tại hoặc chuyển sang chế độ toàn màn hình</string>
<string name="use_exoplayer_decoder_fallback_summary">Bật tùy chọn này nếu bạn gặp sự cố khởi tạo bộ giải mã, vấn đề này sẽ quay trở lại bộ giải mã có mức độ ưu tiên thấp hơn nếu quá trình khởi tạo bộ giải mã chính thất bại. Điều này có thể dẫn đến hiệu suất phát lại kém hơn so với khi sử dụng bộ giải mã chính</string>
<string name="always_use_exoplayer_set_output_surface_workaround_title">Luôn dùng biện pháp thay thế cho bề mặt đầu ra video cho ExoPlayer</string>
<string name="audio_track_type_original">nguyên gốc</string>
@@ -741,8 +741,8 @@
<string name="feed_show_upcoming">Sắp tới</string>
<string name="unknown_audio_track">Không rõ</string>
<string name="select_audio_track_external_players">Chọn bản âm thanh cho máy phát bên ngoài</string>
<string name="settings_category_exoplayer_title">Cài đặt ExoPlayer</string>
<string name="settings_category_exoplayer_summary">Quản lý một số cài đặt ExoPlayer. Những thay đổi này yêu cầu khởi động lại trình phát để có hiệu lực</string>
<string name="settings_category_exoplayer_title">Thiết đặt ExoPlayer</string>
<string name="settings_category_exoplayer_summary">Quản lý một số thiết đặt ExoPlayer. Những thay đổi này yêu cầu khởi động lại trình phát để có hiệu lực</string>
<string name="loading_metadata_title">Đang tải siêu dữ liệu…</string>
<string name="main_tabs_position_title">Vị trí tab chính</string>
<string name="feed_fetch_channel_tabs">Tìm nạp các tab kênh</string>
@@ -753,7 +753,7 @@
<string name="no_live_streams">Không có luồng trực tiếp</string>
<string name="metadata_thumbnails">Ảnh xem trước</string>
<string name="no_streams">Không có luồng</string>
<string name="channel_tab_videos">Các video</string>
<string name="channel_tab_videos">Các cuộn băng</string>
<string name="metadata_subscribers">Người đăng ký</string>
<string name="show_channel_tabs_summary">Những thẻ nào được hiển thị trên các trang kênh</string>
<string name="show_channel_tabs">Thẻ kênh</string>
@@ -763,7 +763,7 @@
<string name="audio_track_name">%1$s %2$s</string>
<string name="next_stream">Luồng tiếp theo</string>
<string name="metadata_subchannel_avatars">Hình đại diện kênh phụ</string>
<string name="open_play_queue">Mở phát hàng chờ</string>
<string name="open_play_queue">Mở hàng chờ phát</string>
<string name="channel_tab_about">Giới thiệu</string>
<string name="replay">Phát lại</string>
<string name="metadata_banners">Băng rôn</string>
@@ -787,7 +787,7 @@
<string name="forward">Tua đi</string>
<string name="channel_tab_albums">Album</string>
<string name="rewind">Tua lại</string>
<string name="share_playlist_with_titles_message">Chia sẻ danh sách phát với các thông tin chi tiết như tên danh sách phát và tiêu đề video hoặc dưới dạng danh sách URL video đơn giản</string>
<string name="share_playlist_with_titles_message">Chia sẻ danh sách phát với các thông tin chi tiết như tên danh sách phát và tiêu đề video hoặc dưới dạng danh sách URL cuốn băng đơn giản</string>
<string name="image_quality_medium">Chất lượng trung bình</string>
<string name="video_details_list_item">- %1$s: %2$s</string>
<string name="image_quality_summary">Chọn chất lượng hình ảnh và chọn có tải chất lượng ảnh hay không, để giảm mức sử dụng dữ liệu và bộ nhớ. Thay đổi xoá cache ảnh cho cả trong bộ nhớ lẫn ổ cứng - %s</string>
@@ -799,15 +799,4 @@
<plurals name="replies">
<item quantity="other">%s hồi đáp</item>
</plurals>
<string name="error_insufficient_storage">Không đủ dung lượng trống trên thiết bị</string>
<string name="settings_category_backup_restore_title">Sao lưu và khôi phục</string>
<string name="reset_settings_title">Đặt lại cài đặt</string>
<string name="reset_settings_summary">Đặt lại tất cả cài đặt về giá trị mặc định</string>
<string name="reset_all_settings">Việc đặt lại tất cả cài đặt sẽ loại bỏ tất cả các cài đặt ưa thích của bạn và khởi động lại ứng dụng.
\n
\nBạn có chắc muốn tiếp tục?</string>
<string name="no">Không</string>
<string name="auto_update_check_description">NewPipe có thể tự động kiểm tra các phiên bản mới theo thời gian và thông báo cho bạn khi chúng có sẵn.
\nBạn có muốn kích hoạt tính năng này không?</string>
<string name="yes"></string>
</resources>

View File

@@ -799,15 +799,4 @@
<string name="show_more">显示较多</string>
<string name="show_less">显示较少</string>
<string name="notification_actions_summary_android13">轻按下方的每个通知操作进行编辑。头三个动作(播放/暂停、上一个和下一个)是系统设置的,不能自定义。</string>
<string name="error_insufficient_storage">设备剩余空间不足</string>
<string name="settings_category_backup_restore_title">备份和还原</string>
<string name="reset_settings_title">重置设置</string>
<string name="reset_settings_summary">将所有设置重置为默认值</string>
<string name="reset_all_settings">重置所有设置会取消你所有的偏好设置并重启应用。
\n
\n你确定想要继续吗</string>
<string name="no"></string>
<string name="auto_update_check_description">NewPipe 可以自动时不时地检查新版本并在新版本可用时通知你。
\n你想开启该功能吗</string>
<string name="yes"></string>
</resources>

View File

@@ -799,15 +799,4 @@
<string name="show_less">摺埋</string>
<string name="show_more">拉開</string>
<string name="notification_actions_summary_android13">撳下面嘅掣去更改對應嘅通知動作。頭三個動作 (播放/暫停、上一個、下一個) 系統預設咗,冇得揀。</string>
<string name="error_insufficient_storage">部機冇晒位</string>
<string name="yes"></string>
<string name="auto_update_check_description">NewPipe 可以周不時自動睇過有冇新版本,一出咗就通知您。
\n您想唔想啟用呢個功能</string>
<string name="reset_settings_title">推翻所有設定</string>
<string name="reset_all_settings">推翻所有設定就會全盤抹走晒您喜好過嘅設定,然後重新開過個 app 個囉噃。
\n
\n您確定要不惜一切推倒重來</string>
<string name="settings_category_backup_restore_title">備份與還原</string>
<string name="no">唔使喇</string>
<string name="reset_settings_summary">顛覆所有設定,光復成預設值重新開始</string>
</resources>

View File

@@ -799,15 +799,4 @@
<string name="show_more">顯示更多</string>
<string name="show_less">顯示較少</string>
<string name="notification_actions_summary_android13">透過點擊下面的每個通知操作來編輯它。前三個動作(播放/暫停、上一個與下一個)由系統設定,無法自訂。</string>
<string name="error_insufficient_storage">裝置上沒有足夠的空間</string>
<string name="settings_category_backup_restore_title">備份與還原</string>
<string name="reset_settings_title">重設設定</string>
<string name="reset_settings_summary">將所有設定重設為預設值</string>
<string name="reset_all_settings">重設所有設定將會放棄您的所有偏好設定並重新啟動應用程式。
\n
\n您確定您想要繼續嗎</string>
<string name="yes"></string>
<string name="no"></string>
<string name="auto_update_check_description">NewPipe 可以隨時自動檢查新版本,並在新版本可用時通知您。
\n您想要啟用此功能嗎</string>
</resources>

View File

@@ -856,5 +856,4 @@
</plurals>
<string name="show_more">Show more</string>
<string name="show_less">Show less</string>
<string name="import_settings_vulnerable_format">The settings in the export being imported use a vulnerable format that was deprecated since NewPipe 0.27.0. Make sure the export being imported is from a trusted source, and prefer using only exports obtained from NewPipe 0.27.0 or newer in the future. Support for importing settings in this vulnerable format will soon be removed completely, and then old versions of NewPipe will not be able to import settings of exports from new versions anymore.</string>
</resources>

View File

@@ -23,6 +23,14 @@
app:singleLineTitle="false"
app:iconSpaceReserved="false" />
<PreferenceScreen
android:key="@string/player_notification_screen_key"
android:fragment="org.schabi.newpipe.settings.PlayerNotificationSettingsFragment"
android:summary="@string/settings_category_player_notification_summary"
android:title="@string/settings_category_player_notification_title"
app:singleLineTitle="false"
app:iconSpaceReserved="false" />
<SwitchPreferenceCompat
android:defaultValue="true"
android:key="@string/show_hold_to_append_key"

View File

@@ -4,14 +4,6 @@
android:key="general_preferences"
android:title="@string/notifications">
<PreferenceScreen
android:key="@string/player_notification_screen_key"
android:fragment="org.schabi.newpipe.settings.PlayerNotificationSettingsFragment"
android:summary="@string/settings_category_player_notification_summary"
android:title="@string/settings_category_player_notification_title"
app:singleLineTitle="false"
app:iconSpaceReserved="false" />
<SwitchPreference
android:defaultValue="false"
android:key="@string/enable_streams_notifications"

View File

@@ -1,106 +0,0 @@
package org.schabi.newpipe.database.playlist;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
import org.schabi.newpipe.local.bookmark.MergedPlaylistManager;
import java.util.ArrayList;
import java.util.List;
public class PlaylistLocalItemTest {
@Test
public void emptyPlaylists() {
final List<PlaylistMetadataEntry> localPlaylists = new ArrayList<>();
final List<PlaylistRemoteEntity> remotePlaylists = new ArrayList<>();
final List<PlaylistLocalItem> mergedPlaylists =
MergedPlaylistManager.merge(localPlaylists, remotePlaylists);
assertEquals(0, mergedPlaylists.size());
}
@Test
public void onlyLocalPlaylists() {
final List<PlaylistMetadataEntry> localPlaylists = new ArrayList<>();
final List<PlaylistRemoteEntity> remotePlaylists = new ArrayList<>();
localPlaylists.add(new PlaylistMetadataEntry(1, "name1", "", false, -1, 0, 1));
localPlaylists.add(new PlaylistMetadataEntry(2, "name2", "", false, -1, 1, 1));
localPlaylists.add(new PlaylistMetadataEntry(3, "name3", "", false, -1, 3, 1));
final List<PlaylistLocalItem> mergedPlaylists =
MergedPlaylistManager.merge(localPlaylists, remotePlaylists);
assertEquals(3, mergedPlaylists.size());
assertEquals(0, mergedPlaylists.get(0).getDisplayIndex());
assertEquals(1, mergedPlaylists.get(1).getDisplayIndex());
assertEquals(3, mergedPlaylists.get(2).getDisplayIndex());
}
@Test
public void onlyRemotePlaylists() {
final List<PlaylistMetadataEntry> localPlaylists = new ArrayList<>();
final List<PlaylistRemoteEntity> remotePlaylists = new ArrayList<>();
remotePlaylists.add(new PlaylistRemoteEntity(
1, "name1", "url1", "", "", 1, 1L));
remotePlaylists.add(new PlaylistRemoteEntity(
2, "name2", "url2", "", "", 2, 1L));
remotePlaylists.add(new PlaylistRemoteEntity(
3, "name3", "url3", "", "", 4, 1L));
final List<PlaylistLocalItem> mergedPlaylists =
MergedPlaylistManager.merge(localPlaylists, remotePlaylists);
assertEquals(3, mergedPlaylists.size());
assertEquals(1, mergedPlaylists.get(0).getDisplayIndex());
assertEquals(2, mergedPlaylists.get(1).getDisplayIndex());
assertEquals(4, mergedPlaylists.get(2).getDisplayIndex());
}
@Test
public void sameIndexWithDifferentName() {
final List<PlaylistMetadataEntry> localPlaylists = new ArrayList<>();
final List<PlaylistRemoteEntity> remotePlaylists = new ArrayList<>();
localPlaylists.add(new PlaylistMetadataEntry(1, "name1", "", false, -1, 0, 1));
localPlaylists.add(new PlaylistMetadataEntry(2, "name2", "", false, -1, 1, 1));
remotePlaylists.add(new PlaylistRemoteEntity(
1, "name3", "url1", "", "", 0, 1L));
remotePlaylists.add(new PlaylistRemoteEntity(
2, "name4", "url2", "", "", 1, 1L));
final List<PlaylistLocalItem> mergedPlaylists =
MergedPlaylistManager.merge(localPlaylists, remotePlaylists);
assertEquals(4, mergedPlaylists.size());
assertTrue(mergedPlaylists.get(0) instanceof PlaylistMetadataEntry);
assertEquals("name1", ((PlaylistMetadataEntry) mergedPlaylists.get(0)).name);
assertTrue(mergedPlaylists.get(1) instanceof PlaylistRemoteEntity);
assertEquals("name3", ((PlaylistRemoteEntity) mergedPlaylists.get(1)).getName());
assertTrue(mergedPlaylists.get(2) instanceof PlaylistMetadataEntry);
assertEquals("name2", ((PlaylistMetadataEntry) mergedPlaylists.get(2)).name);
assertTrue(mergedPlaylists.get(3) instanceof PlaylistRemoteEntity);
assertEquals("name4", ((PlaylistRemoteEntity) mergedPlaylists.get(3)).getName());
}
@Test
public void sameNameWithDifferentIndex() {
final List<PlaylistMetadataEntry> localPlaylists = new ArrayList<>();
final List<PlaylistRemoteEntity> remotePlaylists = new ArrayList<>();
localPlaylists.add(new PlaylistMetadataEntry(1, "name1", "", false, -1, 1, 1));
localPlaylists.add(new PlaylistMetadataEntry(2, "name2", "", false, -1, 3, 1));
remotePlaylists.add(new PlaylistRemoteEntity(
1, "name1", "url1", "", "", 0, 1L));
remotePlaylists.add(new PlaylistRemoteEntity(
2, "name2", "url2", "", "", 2, 1L));
final List<PlaylistLocalItem> mergedPlaylists =
MergedPlaylistManager.merge(localPlaylists, remotePlaylists);
assertEquals(4, mergedPlaylists.size());
assertTrue(mergedPlaylists.get(0) instanceof PlaylistRemoteEntity);
assertEquals("name1", ((PlaylistRemoteEntity) mergedPlaylists.get(0)).getName());
assertTrue(mergedPlaylists.get(1) instanceof PlaylistMetadataEntry);
assertEquals("name1", ((PlaylistMetadataEntry) mergedPlaylists.get(1)).name);
assertTrue(mergedPlaylists.get(2) instanceof PlaylistRemoteEntity);
assertEquals("name2", ((PlaylistRemoteEntity) mergedPlaylists.get(2)).getName());
assertTrue(mergedPlaylists.get(3) instanceof PlaylistMetadataEntry);
assertEquals("name2", ((PlaylistMetadataEntry) mergedPlaylists.get(3)).name);
}
}

View File

@@ -1,10 +1,8 @@
package org.schabi.newpipe.settings
import android.content.SharedPreferences
import com.grack.nanojson.JsonParser
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertThrows
import org.junit.Assert.assertTrue
import org.junit.Assume
import org.junit.Before
@@ -19,8 +17,6 @@ import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
import org.mockito.Mockito.withSettings
import org.mockito.junit.MockitoJUnitRunner
import org.schabi.newpipe.settings.export.BackupFileLocator
import org.schabi.newpipe.settings.export.ImportExportManager
import org.schabi.newpipe.streams.io.StoredFileHelper
import us.shandian.giga.io.FileStream
import java.io.File
@@ -29,25 +25,27 @@ import java.nio.file.Files
import java.util.zip.ZipFile
@RunWith(MockitoJUnitRunner::class)
class ImportExportManagerTest {
class ContentSettingsManagerTest {
companion object {
private val classloader = ImportExportManager::class.java.classLoader!!
private val classloader = ContentSettingsManager::class.java.classLoader!!
}
private lateinit var fileLocator: BackupFileLocator
private lateinit var fileLocator: NewPipeFileLocator
private lateinit var storedFileHelper: StoredFileHelper
@Before
fun setupFileLocator() {
fileLocator = Mockito.mock(BackupFileLocator::class.java, withSettings().stubOnly())
fileLocator = Mockito.mock(NewPipeFileLocator::class.java, withSettings().stubOnly())
storedFileHelper = Mockito.mock(StoredFileHelper::class.java, withSettings().stubOnly())
}
@Test
fun `The settings must be exported successfully in the correct format`() {
val db = File(classloader.getResource("settings/newpipe.db")!!.file)
val newpipeSettings = File.createTempFile("newpipe_", "")
`when`(fileLocator.db).thenReturn(db)
`when`(fileLocator.settings).thenReturn(newpipeSettings)
val expectedPreferences = mapOf("such pref" to "much wow")
val sharedPreferences =
@@ -56,11 +54,11 @@ class ImportExportManagerTest {
val output = File.createTempFile("newpipe_", "")
`when`(storedFileHelper.stream).thenReturn(FileStream(output))
ImportExportManager(fileLocator).exportDatabase(sharedPreferences, storedFileHelper)
ContentSettingsManager(fileLocator).exportDatabase(sharedPreferences, storedFileHelper)
val zipFile = ZipFile(output)
val entries = zipFile.entries().toList()
assertEquals(3, entries.size)
assertEquals(2, entries.size)
zipFile.getInputStream(entries.first { it.name == "newpipe.db" }).use { actual ->
db.inputStream().use { expected ->
@@ -72,11 +70,26 @@ class ImportExportManagerTest {
val actualPreferences = ObjectInputStream(actual).readObject()
assertEquals(expectedPreferences, actualPreferences)
}
}
zipFile.getInputStream(entries.first { it.name == "preferences.json" }).use { actual ->
val actualPreferences = JsonParser.`object`().from(actual)
assertEquals(expectedPreferences, actualPreferences)
}
@Test
fun `Settings file must be deleted`() {
val settings = File.createTempFile("newpipe_", "")
`when`(fileLocator.settings).thenReturn(settings)
ContentSettingsManager(fileLocator).deleteSettingsFile()
assertFalse(settings.exists())
}
@Test
fun `Deleting settings file must do nothing if none exist`() {
val settings = File("non_existent")
`when`(fileLocator.settings).thenReturn(settings)
ContentSettingsManager(fileLocator).deleteSettingsFile()
assertFalse(settings.exists())
}
@Test
@@ -85,7 +98,7 @@ class ImportExportManagerTest {
Assume.assumeTrue(dir.delete())
`when`(fileLocator.dbDir).thenReturn(dir)
ImportExportManager(fileLocator).ensureDbDirectoryExists()
ContentSettingsManager(fileLocator).ensureDbDirectoryExists()
assertTrue(dir.exists())
}
@@ -94,7 +107,7 @@ class ImportExportManagerTest {
val dir = Files.createTempDirectory("newpipe_").toFile()
`when`(fileLocator.dbDir).thenReturn(dir)
ImportExportManager(fileLocator).ensureDbDirectoryExists()
ContentSettingsManager(fileLocator).ensureDbDirectoryExists()
assertTrue(dir.exists())
}
@@ -109,9 +122,9 @@ class ImportExportManagerTest {
`when`(fileLocator.dbShm).thenReturn(dbShm)
`when`(fileLocator.dbWal).thenReturn(dbWal)
val zip = File(classloader.getResource("settings/db_ser_json.zip")?.file!!)
val zip = File(classloader.getResource("settings/newpipe.zip")?.file!!)
`when`(storedFileHelper.stream).thenReturn(FileStream(zip))
val success = ImportExportManager(fileLocator).extractDb(storedFileHelper)
val success = ContentSettingsManager(fileLocator).extractDb(storedFileHelper)
assertTrue(success)
assertFalse(dbJournal.exists())
@@ -128,9 +141,9 @@ class ImportExportManagerTest {
val dbShm = File.createTempFile("newpipe_", "")
`when`(fileLocator.db).thenReturn(db)
val emptyZip = File(classloader.getResource("settings/nodb_noser_nojson.zip")?.file!!)
val emptyZip = File(classloader.getResource("settings/empty.zip")?.file!!)
`when`(storedFileHelper.stream).thenReturn(FileStream(emptyZip))
val success = ImportExportManager(fileLocator).extractDb(storedFileHelper)
val success = ContentSettingsManager(fileLocator).extractDb(storedFileHelper)
assertFalse(success)
assertTrue(dbJournal.exists())
@@ -141,44 +154,41 @@ class ImportExportManagerTest {
@Test
fun `Contains setting must return true if a settings file exists in the zip`() {
val zip = File(classloader.getResource("settings/db_ser_json.zip")?.file!!)
val settings = File.createTempFile("newpipe_", "")
`when`(fileLocator.settings).thenReturn(settings)
val zip = File(classloader.getResource("settings/newpipe.zip")?.file!!)
`when`(storedFileHelper.stream).thenReturn(FileStream(zip))
assertTrue(ImportExportManager(fileLocator).exportHasSerializedPrefs(storedFileHelper))
val contains = ContentSettingsManager(fileLocator).extractSettings(storedFileHelper)
assertTrue(contains)
}
@Test
fun `Contains setting must return false if no settings file exists in the zip`() {
val emptyZip = File(classloader.getResource("settings/nodb_noser_nojson.zip")?.file!!)
fun `Contains setting must return false if a no settings file exists in the zip`() {
val settings = File.createTempFile("newpipe_", "")
`when`(fileLocator.settings).thenReturn(settings)
val emptyZip = File(classloader.getResource("settings/empty.zip")?.file!!)
`when`(storedFileHelper.stream).thenReturn(FileStream(emptyZip))
assertFalse(ImportExportManager(fileLocator).exportHasSerializedPrefs(storedFileHelper))
val contains = ContentSettingsManager(fileLocator).extractSettings(storedFileHelper)
assertFalse(contains)
}
@Test
fun `Preferences must be set from the settings file`() {
val zip = File(classloader.getResource("settings/db_ser_json.zip")?.file!!)
`when`(storedFileHelper.stream).thenReturn(FileStream(zip))
val settings = File(classloader.getResource("settings/newpipe.settings")!!.path)
`when`(fileLocator.settings).thenReturn(settings)
val preferences = Mockito.mock(SharedPreferences::class.java, withSettings().stubOnly())
val editor = Mockito.mock(SharedPreferences.Editor::class.java)
`when`(preferences.edit()).thenReturn(editor)
`when`(editor.commit()).thenReturn(true)
ImportExportManager(fileLocator).loadSerializedPrefs(storedFileHelper, preferences)
ContentSettingsManager(fileLocator).loadSharedPreferences(preferences)
verify(editor, atLeastOnce()).putBoolean(anyString(), anyBoolean())
verify(editor, atLeastOnce()).putString(anyString(), anyString())
verify(editor, atLeastOnce()).putInt(anyString(), anyInt())
}
@Test
fun `Importing preferences with a serialization injected class should fail`() {
val emptyZip = File(classloader.getResource("settings/db_vulnser_json.zip")?.file!!)
`when`(storedFileHelper.stream).thenReturn(FileStream(emptyZip))
val preferences = Mockito.mock(SharedPreferences::class.java, withSettings().stubOnly())
assertThrows(ClassNotFoundException::class.java) {
ImportExportManager(fileLocator).loadSerializedPrefs(storedFileHelper, preferences)
}
}
}

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