Compare commits
138 Commits
18251dc062
...
5a385185cf
Author | SHA1 | Date |
---|---|---|
Siddhesh Naik | 5a385185cf | |
Stypox | 9828586762 | |
Hosted Weblate | 8caaa6d297 | |
Stypox | 83ca6b9468 | |
Stypox | 24e65ef018 | |
Stypox | a69bbab732 | |
Stypox | a557ac3c7b | |
Stypox | d61b4b89ea | |
Stypox | b8daf16b92 | |
Stypox | caa3812e13 | |
Hosted Weblate | 23a087c498 | |
Stypox | c3c39a7b24 | |
Stypox | 00770fc634 | |
Stypox | 5bf77160f7 | |
Stypox | d9da84c412 | |
Audric V | b3a6318672 | |
Stypox | 67b41b970d | |
Stypox | 3738e30949 | |
Stypox | 0ba73b11c1 | |
bg1722 | 13baaa31cd | |
TobiGr | f0db2aa43c | |
Stypox | f704721b59 | |
Stypox | 7abf0f4886 | |
Stypox | c915b6e68b | |
Hosted Weblate | 0b28c688c6 | |
Stypox | 2756ef6d2f | |
Stypox | 7da1d30010 | |
Stypox | 8e192acb63 | |
Stypox | d8423499dc | |
TobiGr | 974167fcb8 | |
Stypox | 6afdbd6fd3 | |
Stypox | d8668ed226 | |
Stypox | d75a6eaa41 | |
Stypox | 235fb92638 | |
Stypox | ea18b4ea1f | |
Stypox | 58f5ec0181 | |
pratyaksh1610 | e42c9abdde | |
Stypox | 5e7ad6ffd1 | |
Stypox | 4c8238874e | |
Stypox | 38d4887901 | |
Stypox | c9051d33c1 | |
Stypox | 3cc0205def | |
Stypox | 90979e2a81 | |
Stypox | e66e1b542c | |
Stypox | 92e9c3e42e | |
Stypox | 4591c09637 | |
Stypox | e1ce3fef1b | |
Stypox | 3c0a200f7b | |
bg1722 | bef5907ec3 | |
Stypox | f0beb662aa | |
Stypox | 92402685f8 | |
Stypox | 3703fed1a5 | |
Tobi | a3bbbf03b4 | |
TobiGr | 1d3a69a29f | |
Stypox | 10c57b15da | |
Stypox | b85f7a6747 | |
Stypox | 3f94e7b638 | |
Stypox | 2af95cc1d4 | |
Stypox | cefdefdfd2 | |
Stypox | 37f7fa7ef4 | |
Stypox | e687eb5631 | |
Stypox | 88c3af7647 | |
ge78fug | ddd6c8cbf1 | |
Stypox | 81220f90d6 | |
Stypox | e0268a91ad | |
Stypox | 29e4135aaa | |
Stypox | 5d9adce40d | |
Stypox | d3afde8789 | |
Stypox | d8a5d5545d | |
Stypox | bed3516687 | |
Stypox | 3a014d8d46 | |
Stypox | 58ae7fbccb | |
Stypox | b06a9618d4 | |
Stypox | 434c4a5cbc | |
TobiGr | c34d30dc17 | |
TobiGr | 0d4c1bee3f | |
Tobi | 34a25d0be3 | |
Mohammed Anas | 3134f5e747 | |
Hosted Weblate | 1732584e5e | |
Tobi | f50cafbac1 | |
Mohammed Anas | bc7c3f48ad | |
Tobi | b760419fd5 | |
Tobi | 5cf3c58d0e | |
TobiGr | 206d1b6db4 | |
CloudyRowly | 2e318b8b03 | |
Isira Seneviratne | 5bdb6f18d6 | |
Isira Seneviratne | 2e53a99361 | |
Isira Seneviratne | bec18e13d3 | |
Tobi | 7edd471ec5 | |
Hosted Weblate | e6a4a3fa4f | |
Tobi | de2a139340 | |
Ikko Eltociear Ashimine | 9d6ac67c46 | |
Profpatsch | 32d2606a65 | |
Profpatsch | 575e809004 | |
Roshan Jossy | 66e8e2a696 | |
Stypox | 55373c95d9 | |
Stypox | 04bdc1cc0b | |
TobiGr | df2e0be08d | |
TobiGr | ff1aca272e | |
TobiGr | f2e352832a | |
vincetzr | ad0855ac83 | |
Vincent Tanumihardja | d7ef9b1f0c | |
Vincent Tanumihardja | 40a3e1b18a | |
Vincent Tanumihardja | 25a73090f5 | |
Vincent Tanumihardja | a239a26b17 | |
Vincent Tanumihardja | 06d256294f | |
Vincent Tanumihardja | 81ad50e82a | |
Zhidong Piao | 23de9bf93e | |
Vincent Tanumihardja | 5c46412faa | |
Vincent Tanumihardja | 076e9eee01 | |
Vincent Tanumihardja | 2103a04092 | |
Vincent Tanumihardja | 58517d1d27 | |
Vincent Tanumihardja | aa1847189b | |
Vincent Tanumihardja | 5d101e7b88 | |
TobiGr | 9118ecd68f | |
TobiGr | 15fd47c7f2 | |
Yingwei Zheng | ef40ac7bb3 | |
Yingwei Zheng | 881d04ba1e | |
TobiGr | 4af5b5f6f2 | |
TobiGr | 90f0809029 | |
GGAutomaton | 8ad7bf60d7 | |
GGAutomaton | 898a936064 | |
GGAutomaton | 4e401bc059 | |
GGAutomaton | 9ecef6f011 | |
GGAutomaton | ba394a7ab4 | |
GGAutomaton | d32490a4be | |
GGAutomaton | 6526ff1612 | |
GGAutomaton | bb5390d63a | |
GGAutomaton | bd1aae8d66 | |
GGAutomaton | c24aed054f | |
GGAutomaton | 0aa08a5e40 | |
GGAutomaton | 3c48825699 | |
GGAutomaton | bfb56b4144 | |
GGAutomaton | ba8370bcfd | |
GGAutomaton | 813f55152a | |
GGAutomaton | 270a541a7c | |
GGAutomaton | c34549a47d | |
GGAutomaton | 96d6b309ec |
|
@ -1,6 +1,3 @@
|
|||
name: Question
|
||||
description: Ask about anything NewPipe-related
|
||||
labels: [question]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
|
|
|
@ -37,7 +37,7 @@ jobs:
|
|||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: gradle/wrapper-validation-action@v1
|
||||
- uses: gradle/wrapper-validation-action@v2
|
||||
|
||||
- name: create and checkout branch
|
||||
# push events already checked out the branch
|
||||
|
@ -123,7 +123,7 @@ jobs:
|
|||
cache: 'gradle'
|
||||
|
||||
- name: Cache SonarCloud packages
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.sonar/cache
|
||||
key: ${{ runner.os }}-sonar
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
name: "PR size labeler"
|
||||
on: [pull_request]
|
||||
on: [pull_request_target]
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
|
|
@ -22,9 +22,10 @@
|
|||
|
||||
*Read this document in other languages: [Deutsch](doc/README.de.md), [English](README.md), [Español](doc/README.es.md), [Français](doc/README.fr.md), [हिन्दी](doc/README.hi.md), [Italiano](doc/README.it.md), [한국어](doc/README.ko.md), [Português Brasil](doc/README.pt_BR.md), [Polski](doc/README.pl.md), [ਪੰਜਾਬੀ ](doc/README.pa.md), [日本語](doc/README.ja.md), [Română](doc/README.ro.md), [Soomaali](doc/README.so.md), [Türkçe](doc/README.tr.md), [正體中文](doc/README.zh_TW.md), [অসমীয়া](doc/README.asm.md), [Српски](doc/README.sr.md)*
|
||||
|
||||
<b>WARNING: THIS APP IS IN BETA, SO YOU MAY ENCOUNTER BUGS. IF YOU DO, OPEN AN ISSUE IN OUR GITHUB REPOSITORY BY FILLING OUT THE ISSUE TEMPLATE.</b>
|
||||
|
||||
<b>PUTTING NEWPIPE, OR ANY FORK OF IT, INTO THE GOOGLE PLAY STORE VIOLATES THEIR TERMS AND CONDITIONS.</b>
|
||||
> [!warning]
|
||||
> <b>THIS APP IS IN BETA, SO YOU MAY ENCOUNTER BUGS. IF YOU DO, OPEN AN ISSUE IN OUR GITHUB REPOSITORY BY FILLING OUT THE ISSUE TEMPLATE.</b>
|
||||
>
|
||||
> <b>PUTTING NEWPIPE, OR ANY FORK OF IT, INTO THE GOOGLE PLAY STORE VIOLATES THEIR TERMS AND CONDITIONS.</b>
|
||||
|
||||
## Screenshots
|
||||
|
||||
|
|
|
@ -20,8 +20,8 @@ android {
|
|||
resValue "string", "app_name", "NewPipe"
|
||||
minSdk 21
|
||||
targetSdk 33
|
||||
versionCode 996
|
||||
versionName "0.26.1"
|
||||
versionCode 997
|
||||
versionName "0.27.0"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
|
@ -203,7 +203,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.23.1'
|
||||
implementation 'com.github.TeamNewPipe:NewPipeExtractor:v0.24.0'
|
||||
implementation 'com.github.TeamNewPipe:NoNonsense-FilePicker:5.0.0'
|
||||
|
||||
/** Checkstyle **/
|
||||
|
|
|
@ -0,0 +1,737 @@
|
|||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 8,
|
||||
"identityHash": "012fc8e7ad3333f1597347f34e76a513",
|
||||
"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)",
|
||||
"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
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"uid"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_playlists_name",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"name"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_playlists_name` ON `${TABLE_NAME}` (`name`)"
|
||||
}
|
||||
],
|
||||
"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, `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": "streamCount",
|
||||
"columnName": "stream_count",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"uid"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_remote_playlists_name",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"name"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_remote_playlists_name` ON `${TABLE_NAME}` (`name`)"
|
||||
},
|
||||
{
|
||||
"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, '012fc8e7ad3333f1597347f34e76a513')"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,730 @@
|
|||
{
|
||||
"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')"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -8,10 +8,14 @@ import androidx.test.core.app.ApplicationProvider
|
|||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotEquals
|
||||
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
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
|
@ -20,13 +24,17 @@ 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 = 0
|
||||
private const val DEFAULT_SECOND_SERVICE_ID = 1
|
||||
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
|
||||
|
@ -106,6 +114,20 @@ class DatabaseMigrationTest {
|
|||
Migrations.MIGRATION_6_7
|
||||
)
|
||||
|
||||
testHelper.runMigrationsAndValidate(
|
||||
AppDatabase.DATABASE_NAME,
|
||||
Migrations.DB_VER_8,
|
||||
true,
|
||||
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()
|
||||
|
||||
|
@ -140,6 +162,157 @@ class DatabaseMigrationTest {
|
|||
assertNull(secondStreamFromMigratedDatabase.isUploadDateApproximation)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun migrateDatabaseFrom7to8() {
|
||||
val databaseInV7 = testHelper.createDatabase(AppDatabase.DATABASE_NAME, Migrations.DB_VER_7)
|
||||
|
||||
val defaultSearch1 = " abc "
|
||||
val defaultSearch2 = " abc"
|
||||
|
||||
val serviceId = DEFAULT_SERVICE_ID // YouTube
|
||||
// Use id different to YouTube because two searches with the same query
|
||||
// but different service are considered not equal.
|
||||
val otherServiceId = ServiceList.SoundCloud.serviceId
|
||||
|
||||
databaseInV7.run {
|
||||
insert(
|
||||
"search_history", SQLiteDatabase.CONFLICT_FAIL,
|
||||
ContentValues().apply {
|
||||
put("service_id", serviceId)
|
||||
put("search", defaultSearch1)
|
||||
}
|
||||
)
|
||||
insert(
|
||||
"search_history", SQLiteDatabase.CONFLICT_FAIL,
|
||||
ContentValues().apply {
|
||||
put("service_id", serviceId)
|
||||
put("search", defaultSearch2)
|
||||
}
|
||||
)
|
||||
insert(
|
||||
"search_history", SQLiteDatabase.CONFLICT_FAIL,
|
||||
ContentValues().apply {
|
||||
put("service_id", otherServiceId)
|
||||
put("search", defaultSearch1)
|
||||
}
|
||||
)
|
||||
insert(
|
||||
"search_history", SQLiteDatabase.CONFLICT_FAIL,
|
||||
ContentValues().apply {
|
||||
put("service_id", otherServiceId)
|
||||
put("search", defaultSearch2)
|
||||
}
|
||||
)
|
||||
close()
|
||||
}
|
||||
|
||||
testHelper.runMigrationsAndValidate(
|
||||
AppDatabase.DATABASE_NAME, Migrations.DB_VER_8,
|
||||
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()
|
||||
|
||||
assertEquals(2, listFromDB.size)
|
||||
assertEquals("abc", listFromDB[0].search)
|
||||
assertEquals("abc", listFromDB[1].search)
|
||||
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(),
|
||||
|
|
|
@ -60,6 +60,8 @@ import io.reactivex.rxjava3.plugins.RxJavaPlugins;
|
|||
public class App extends Application {
|
||||
public static final String PACKAGE_NAME = BuildConfig.APPLICATION_ID;
|
||||
private static final String TAG = App.class.toString();
|
||||
|
||||
private boolean isFirstRun = false;
|
||||
private static App app;
|
||||
|
||||
@NonNull
|
||||
|
@ -85,7 +87,13 @@ public class App extends Application {
|
|||
return;
|
||||
}
|
||||
|
||||
// Initialize settings first because others inits can use its values
|
||||
// check if the last used preference version is set
|
||||
// to determine whether this is the first app run
|
||||
final int lastUsedPrefVersion = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
.getInt(getString(R.string.last_used_preferences_version), -1);
|
||||
isFirstRun = lastUsedPrefVersion == -1;
|
||||
|
||||
// Initialize settings first because other initializations can use its values
|
||||
NewPipeSettings.initSettings(this);
|
||||
|
||||
NewPipe.init(getDownloader(),
|
||||
|
@ -255,4 +263,7 @@ public class App extends Application {
|
|||
return false;
|
||||
}
|
||||
|
||||
public boolean isFirstRun() {
|
||||
return isFirstRun;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -79,6 +79,7 @@ import org.schabi.newpipe.player.Player;
|
|||
import org.schabi.newpipe.player.event.OnKeyDownListener;
|
||||
import org.schabi.newpipe.player.helper.PlayerHolder;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.settings.UpdateSettingsFragment;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
import org.schabi.newpipe.util.DeviceUtils;
|
||||
import org.schabi.newpipe.util.KioskTranslator;
|
||||
|
@ -86,6 +87,7 @@ import org.schabi.newpipe.util.Localization;
|
|||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.PeertubeHelper;
|
||||
import org.schabi.newpipe.util.PermissionHelper;
|
||||
import org.schabi.newpipe.util.ReleaseVersionUtil;
|
||||
import org.schabi.newpipe.util.SerializedCache;
|
||||
import org.schabi.newpipe.util.ServiceHelper;
|
||||
import org.schabi.newpipe.util.StateSaver;
|
||||
|
@ -167,6 +169,11 @@ public class MainActivity extends AppCompatActivity {
|
|||
// if this is enabled by the user.
|
||||
NotificationWorker.initialize(this);
|
||||
}
|
||||
if (!UpdateSettingsFragment.wasUserAskedForConsent(this)
|
||||
&& !App.getApp().isFirstRun()
|
||||
&& ReleaseVersionUtil.INSTANCE.isReleaseApk()) {
|
||||
UpdateSettingsFragment.askForConsentToUpdateChecks(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -176,7 +183,8 @@ public class MainActivity extends AppCompatActivity {
|
|||
final App app = App.getApp();
|
||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(app);
|
||||
|
||||
if (prefs.getBoolean(app.getString(R.string.update_app_key), true)) {
|
||||
if (prefs.getBoolean(app.getString(R.string.update_app_key), false)
|
||||
&& prefs.getBoolean(app.getString(R.string.update_check_consent_key), false)) {
|
||||
// Start the worker which is checking all conditions
|
||||
// and eventually searching for a new version.
|
||||
NewVersionWorker.enqueueNewVersionCheckingWork(app, false);
|
||||
|
|
|
@ -7,6 +7,8 @@ import static org.schabi.newpipe.database.Migrations.MIGRATION_3_4;
|
|||
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;
|
||||
|
@ -27,7 +29,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_5_6, MIGRATION_6_7, MIGRATION_7_8, MIGRATION_8_9)
|
||||
.build();
|
||||
}
|
||||
|
||||
|
|
|
@ -20,9 +20,7 @@ import com.grack.nanojson.JsonParser
|
|||
import com.grack.nanojson.JsonParserException
|
||||
import org.schabi.newpipe.extractor.downloader.Response
|
||||
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException
|
||||
import org.schabi.newpipe.util.ReleaseVersionUtil.coerceUpdateCheckExpiry
|
||||
import org.schabi.newpipe.util.ReleaseVersionUtil.isLastUpdateCheckExpired
|
||||
import org.schabi.newpipe.util.ReleaseVersionUtil.isReleaseApk
|
||||
import org.schabi.newpipe.util.ReleaseVersionUtil
|
||||
import java.io.IOException
|
||||
|
||||
class NewVersionWorker(
|
||||
|
@ -84,7 +82,7 @@ class NewVersionWorker(
|
|||
@Throws(IOException::class, ReCaptchaException::class)
|
||||
private fun checkNewVersion() {
|
||||
// Check if the current apk is a github one or not.
|
||||
if (!isReleaseApk()) {
|
||||
if (!ReleaseVersionUtil.isReleaseApk) {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -93,7 +91,7 @@ class NewVersionWorker(
|
|||
// Check if the last request has happened a certain time ago
|
||||
// to reduce the number of API requests.
|
||||
val expiry = prefs.getLong(applicationContext.getString(R.string.update_expiry_key), 0)
|
||||
if (!isLastUpdateCheckExpired(expiry)) {
|
||||
if (!ReleaseVersionUtil.isLastUpdateCheckExpired(expiry)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
@ -108,7 +106,7 @@ class NewVersionWorker(
|
|||
try {
|
||||
// Store a timestamp which needs to be exceeded,
|
||||
// before a new request to the API is made.
|
||||
val newExpiry = coerceUpdateCheckExpiry(response.getHeader("expires"))
|
||||
val newExpiry = ReleaseVersionUtil.coerceUpdateCheckExpiry(response.getHeader("expires"))
|
||||
prefs.edit {
|
||||
putLong(applicationContext.getString(R.string.update_expiry_key), newExpiry)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package org.schabi.newpipe.database;
|
||||
|
||||
import static org.schabi.newpipe.database.Migrations.DB_VER_7;
|
||||
import static org.schabi.newpipe.database.Migrations.DB_VER_9;
|
||||
|
||||
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_7
|
||||
version = DB_VER_9
|
||||
)
|
||||
public abstract class AppDatabase extends RoomDatabase {
|
||||
public static final String DATABASE_NAME = "newpipe.db";
|
||||
|
|
|
@ -25,6 +25,8 @@ public final class Migrations {
|
|||
public static final int DB_VER_5 = 5;
|
||||
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;
|
||||
|
@ -186,7 +188,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");
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -235,6 +237,71 @@ public final class Migrations {
|
|||
}
|
||||
};
|
||||
|
||||
public static final Migration MIGRATION_7_8 = new Migration(DB_VER_7, DB_VER_8) {
|
||||
@Override
|
||||
public void migrate(@NonNull final SupportSQLiteDatabase database) {
|
||||
database.execSQL("DELETE FROM search_history WHERE id NOT IN (SELECT id FROM (SELECT "
|
||||
+ "MIN(id) as id FROM search_history GROUP BY trim(search), service_id ) tmp)");
|
||||
database.execSQL("UPDATE search_history SET search = trim(search)");
|
||||
}
|
||||
};
|
||||
|
||||
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() {
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,12 +13,17 @@ 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, streamCount);
|
||||
super(uid, name, thumbnailUrl, isThumbnailPermanent, thumbnailStreamId, displayIndex,
|
||||
streamCount);
|
||||
this.timesStreamIsContained = timesStreamIsContained;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,22 +1,13 @@
|
|||
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();
|
||||
|
||||
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());
|
||||
}
|
||||
long getDisplayIndex();
|
||||
|
||||
long getUid();
|
||||
|
||||
void setDisplayIndex(long displayIndex);
|
||||
}
|
||||
|
|
|
@ -2,27 +2,40 @@ 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)
|
||||
public final long uid;
|
||||
private 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 long streamCount) {
|
||||
final boolean isThumbnailPermanent, final long thumbnailStreamId,
|
||||
final long displayIndex, final long streamCount) {
|
||||
this.uid = uid;
|
||||
this.name = name;
|
||||
this.thumbnailUrl = thumbnailUrl;
|
||||
this.isThumbnailPermanent = isThumbnailPermanent;
|
||||
this.thumbnailStreamId = thumbnailStreamId;
|
||||
this.displayIndex = displayIndex;
|
||||
this.streamCount = streamCount;
|
||||
}
|
||||
|
||||
|
@ -35,4 +48,27 @@ 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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ 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;
|
||||
|
@ -36,4 +37,17 @@ 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ 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;
|
||||
|
@ -31,10 +32,18 @@ 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")
|
||||
|
|
|
@ -18,10 +18,12 @@ 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;
|
||||
|
@ -91,7 +93,9 @@ public interface PlaylistStreamDAO extends BasicDAO<PlaylistStreamEntity> {
|
|||
Flowable<List<PlaylistStreamEntry>> getOrderedStreamsOf(long playlistId);
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT " + PLAYLIST_ID + ", " + PLAYLIST_NAME + ","
|
||||
@Query("SELECT " + PLAYLIST_ID + ", " + PLAYLIST_NAME + ", "
|
||||
+ PLAYLIST_THUMBNAIL_PERMANENT + ", " + PLAYLIST_THUMBNAIL_STREAM_ID + ", "
|
||||
+ PLAYLIST_DISPLAY_INDEX + ", "
|
||||
|
||||
+ " CASE WHEN " + PLAYLIST_THUMBNAIL_STREAM_ID + " = "
|
||||
+ PlaylistEntity.DEFAULT_THUMBNAIL_ID + " THEN " + "'" + DEFAULT_THUMBNAIL + "'"
|
||||
|
@ -105,7 +109,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_NAME + " COLLATE NOCASE ASC")
|
||||
+ " ORDER BY " + PLAYLIST_DISPLAY_INDEX)
|
||||
Flowable<List<PlaylistMetadataEntry>> getPlaylistMetadata();
|
||||
|
||||
@RewriteQueriesToDropUnusedColumns
|
||||
|
@ -126,8 +130,9 @@ public interface PlaylistStreamDAO extends BasicDAO<PlaylistStreamEntity> {
|
|||
Flowable<List<PlaylistStreamEntry>> getStreamsWithoutDuplicates(long playlistId);
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT " + PLAYLIST_TABLE + "." + PLAYLIST_ID + ", "
|
||||
+ PLAYLIST_NAME + ", "
|
||||
@Query("SELECT " + PLAYLIST_TABLE + "." + PLAYLIST_ID + ", " + PLAYLIST_NAME + ", "
|
||||
+ PLAYLIST_THUMBNAIL_PERMANENT + ", " + PLAYLIST_THUMBNAIL_STREAM_ID + ", "
|
||||
+ PLAYLIST_DISPLAY_INDEX + ", "
|
||||
|
||||
+ " CASE WHEN " + PLAYLIST_THUMBNAIL_STREAM_ID + " = "
|
||||
+ PlaylistEntity.DEFAULT_THUMBNAIL_ID + " THEN " + "'" + DEFAULT_THUMBNAIL + "'"
|
||||
|
@ -149,6 +154,6 @@ public interface PlaylistStreamDAO extends BasicDAO<PlaylistStreamEntity> {
|
|||
+ " AND :streamUrl = :streamUrl"
|
||||
|
||||
+ " GROUP BY " + JOIN_PLAYLIST_ID
|
||||
+ " ORDER BY " + PLAYLIST_NAME + " COLLATE NOCASE ASC")
|
||||
+ " ORDER BY " + PLAYLIST_DISPLAY_INDEX)
|
||||
Flowable<List<PlaylistDuplicatesEntry>> getPlaylistDuplicatesMetadata(String streamUrl);
|
||||
}
|
||||
|
|
|
@ -2,16 +2,15 @@ package org.schabi.newpipe.database.playlist.model;
|
|||
|
||||
import androidx.room.ColumnInfo;
|
||||
import androidx.room.Entity;
|
||||
import androidx.room.Index;
|
||||
import androidx.room.Ignore;
|
||||
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,
|
||||
indices = {@Index(value = {PLAYLIST_NAME})})
|
||||
@Entity(tableName = PLAYLIST_TABLE)
|
||||
public class PlaylistEntity {
|
||||
|
||||
public static final String DEFAULT_THUMBNAIL = "drawable://"
|
||||
|
@ -22,6 +21,7 @@ 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,11 +38,24 @@ 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 thumbnailStreamId, final long displayIndex) {
|
||||
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() {
|
||||
|
@ -77,4 +90,11 @@ public class PlaylistEntity {
|
|||
this.isThumbnailPermanent = isThumbnailSet;
|
||||
}
|
||||
|
||||
public long getDisplayIndex() {
|
||||
return displayIndex;
|
||||
}
|
||||
|
||||
public void setDisplayIndex(final long displayIndex) {
|
||||
this.displayIndex = displayIndex;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,7 +21,6 @@ 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 {
|
||||
|
@ -32,6 +31,7 @@ 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,6 +53,9 @@ 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;
|
||||
|
||||
|
@ -67,6 +70,19 @@ 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(),
|
||||
|
@ -93,6 +109,7 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem {
|
|||
&& TextUtils.equals(getUploader(), info.getUploaderName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getUid() {
|
||||
return uid;
|
||||
}
|
||||
|
@ -141,6 +158,16 @@ 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;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
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;
|
||||
|
@ -95,11 +96,12 @@ public class SubscriptionEntity {
|
|||
this.name = name;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getAvatarUrl() {
|
||||
return avatarUrl;
|
||||
}
|
||||
|
||||
public void setAvatarUrl(final String avatarUrl) {
|
||||
public void setAvatarUrl(@Nullable final String avatarUrl) {
|
||||
this.avatarUrl = avatarUrl;
|
||||
}
|
||||
|
||||
|
|
|
@ -7,8 +7,6 @@ import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
|||
import android.app.Activity;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.DialogInterface.OnDismissListener;
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.content.SharedPreferences;
|
||||
|
@ -16,6 +14,7 @@ import android.net.Uri;
|
|||
import android.os.Bundle;
|
||||
import android.os.Environment;
|
||||
import android.os.IBinder;
|
||||
import android.provider.Settings;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
|
@ -112,14 +111,11 @@ public class DownloadDialog extends DialogFragment
|
|||
@State
|
||||
int selectedSubtitleIndex = 0; // default to the first item
|
||||
|
||||
@Nullable
|
||||
private OnDismissListener onDismissListener = null;
|
||||
|
||||
private StoredDirectoryHelper mainStorageAudio = null;
|
||||
private StoredDirectoryHelper mainStorageVideo = null;
|
||||
private DownloadManager downloadManager = null;
|
||||
private ActionMenuItemView okButton = null;
|
||||
private Context context;
|
||||
private Context context = null;
|
||||
private boolean askForSavePath;
|
||||
|
||||
private AudioTrackAdapter audioTrackAdapter;
|
||||
|
@ -147,7 +143,6 @@ public class DownloadDialog extends DialogFragment
|
|||
registerForActivityResult(
|
||||
new StartActivityForResult(), this::requestDownloadPickVideoFolderResult);
|
||||
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Instance creation
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
@ -195,13 +190,6 @@ public class DownloadDialog extends DialogFragment
|
|||
this.selectedVideoIndex = ListHelper.getDefaultResolutionIndex(context, videoStreams);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param onDismissListener the listener to call in {@link #onDismiss(DialogInterface)}
|
||||
*/
|
||||
public void setOnDismissListener(@Nullable final OnDismissListener onDismissListener) {
|
||||
this.onDismissListener = onDismissListener;
|
||||
}
|
||||
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Android lifecycle
|
||||
|
@ -221,6 +209,8 @@ public class DownloadDialog extends DialogFragment
|
|||
return;
|
||||
}
|
||||
|
||||
// context will remain null if dismiss() was called above, allowing to check whether the
|
||||
// dialog is being dismissed in onViewCreated()
|
||||
context = getContext();
|
||||
|
||||
setStyle(STYLE_NO_TITLE, ThemeHelper.getDialogTheme(context));
|
||||
|
@ -305,6 +295,9 @@ public class DownloadDialog extends DialogFragment
|
|||
@Nullable final Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
dialogBinding = DownloadDialogBinding.bind(view);
|
||||
if (context == null) {
|
||||
return; // the dialog is being dismissed, see the call to dismiss() in onCreate()
|
||||
}
|
||||
|
||||
dialogBinding.fileName.setText(FilenameUtils.createFilename(getContext(),
|
||||
currentInfo.getName()));
|
||||
|
@ -364,14 +357,6 @@ public class DownloadDialog extends DialogFragment
|
|||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDismiss(@NonNull final DialogInterface dialog) {
|
||||
super.onDismiss(dialog);
|
||||
if (onDismissListener != null) {
|
||||
onDismissListener.onDismiss(dialog);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
|
@ -565,7 +550,6 @@ public class DownloadDialog extends DialogFragment
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Listeners
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
@ -784,6 +768,7 @@ public class DownloadDialog extends DialogFragment
|
|||
final StoredDirectoryHelper mainStorage;
|
||||
final MediaFormat format;
|
||||
final String selectedMediaType;
|
||||
final long size;
|
||||
|
||||
// first, build the filename and get the output folder (if possible)
|
||||
// later, run a very very very large file checking logic
|
||||
|
@ -795,6 +780,7 @@ public class DownloadDialog extends DialogFragment
|
|||
selectedMediaType = getString(R.string.last_download_type_audio_key);
|
||||
mainStorage = mainStorageAudio;
|
||||
format = audioStreamsAdapter.getItem(selectedAudioIndex).getFormat();
|
||||
size = getWrappedAudioStreams().getSizeInBytes(selectedAudioIndex);
|
||||
if (format == MediaFormat.WEBMA_OPUS) {
|
||||
mimeTmp = "audio/ogg";
|
||||
filenameTmp += "opus";
|
||||
|
@ -807,6 +793,7 @@ public class DownloadDialog extends DialogFragment
|
|||
selectedMediaType = getString(R.string.last_download_type_video_key);
|
||||
mainStorage = mainStorageVideo;
|
||||
format = videoStreamsAdapter.getItem(selectedVideoIndex).getFormat();
|
||||
size = wrappedVideoStreams.getSizeInBytes(selectedVideoIndex);
|
||||
if (format != null) {
|
||||
mimeTmp = format.mimeType;
|
||||
filenameTmp += format.getSuffix();
|
||||
|
@ -816,6 +803,7 @@ public class DownloadDialog extends DialogFragment
|
|||
selectedMediaType = getString(R.string.last_download_type_subtitle_key);
|
||||
mainStorage = mainStorageVideo; // subtitle & video files go together
|
||||
format = subtitleStreamsAdapter.getItem(selectedSubtitleIndex).getFormat();
|
||||
size = wrappedSubtitleStreams.getSizeInBytes(selectedSubtitleIndex);
|
||||
if (format != null) {
|
||||
mimeTmp = format.mimeType;
|
||||
}
|
||||
|
@ -871,6 +859,21 @@ 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);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// check for existing file with the same name
|
||||
checkSelectedDownload(mainStorage, mainStorage.findFile(filenameTmp), filenameTmp,
|
||||
mimeTmp);
|
||||
|
|
|
@ -6,6 +6,7 @@ package org.schabi.newpipe.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"),
|
||||
|
|
|
@ -220,7 +220,7 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
|||
public void commitPlaylistTabs() {
|
||||
pagerAdapter.getLocalPlaylistFragments()
|
||||
.stream()
|
||||
.forEach(LocalPlaylistFragment::commitChanges);
|
||||
.forEach(LocalPlaylistFragment::saveImmediate);
|
||||
}
|
||||
|
||||
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#commitChanges()}.
|
||||
* {@link LocalPlaylistFragment#saveImmediate()}.
|
||||
* The fragments are removed when {@link LocalPlaylistFragment#onDestroy()} is called.
|
||||
*/
|
||||
private final List<LocalPlaylistFragment> localPlaylistFragments = new ArrayList<>();
|
||||
|
|
|
@ -64,7 +64,7 @@ public abstract class BaseDescriptionFragment extends BaseFragment {
|
|||
|
||||
/**
|
||||
* Get the description to display.
|
||||
* @return description object
|
||||
* @return description object, if available
|
||||
*/
|
||||
@Nullable
|
||||
protected abstract Description getDescription();
|
||||
|
@ -73,7 +73,7 @@ public abstract class BaseDescriptionFragment extends BaseFragment {
|
|||
* Get the streaming service. Used for generating description links.
|
||||
* @return streaming service
|
||||
*/
|
||||
@Nullable
|
||||
@NonNull
|
||||
protected abstract StreamingService getService();
|
||||
|
||||
/**
|
||||
|
@ -93,7 +93,7 @@ public abstract class BaseDescriptionFragment extends BaseFragment {
|
|||
* Get the list of tags to display below the description.
|
||||
* @return tag list
|
||||
*/
|
||||
@Nullable
|
||||
@NonNull
|
||||
public abstract List<String> getTags();
|
||||
|
||||
/**
|
||||
|
@ -158,7 +158,7 @@ public abstract class BaseDescriptionFragment extends BaseFragment {
|
|||
final LinearLayout layout,
|
||||
final boolean linkifyContent,
|
||||
@StringRes final int type,
|
||||
@Nullable final String content) {
|
||||
@NonNull final String content) {
|
||||
if (isBlank(content)) {
|
||||
return;
|
||||
}
|
||||
|
@ -221,16 +221,12 @@ public abstract class BaseDescriptionFragment extends BaseFragment {
|
|||
urls.append(imageSizeToText(image.getWidth()));
|
||||
} else {
|
||||
switch (image.getEstimatedResolutionLevel()) {
|
||||
case LOW:
|
||||
urls.append(getString(R.string.image_quality_low));
|
||||
break;
|
||||
default: // unreachable, Image.ResolutionLevel.UNKNOWN is already filtered out
|
||||
case MEDIUM:
|
||||
urls.append(getString(R.string.image_quality_medium));
|
||||
break;
|
||||
case HIGH:
|
||||
urls.append(getString(R.string.image_quality_high));
|
||||
break;
|
||||
case LOW -> urls.append(getString(R.string.image_quality_low));
|
||||
case MEDIUM -> urls.append(getString(R.string.image_quality_medium));
|
||||
case HIGH -> urls.append(getString(R.string.image_quality_high));
|
||||
default -> {
|
||||
// unreachable, Image.ResolutionLevel.UNKNOWN is already filtered out
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -255,7 +251,7 @@ public abstract class BaseDescriptionFragment extends BaseFragment {
|
|||
private void addTagsMetadataItem(final LayoutInflater inflater, final LinearLayout layout) {
|
||||
final List<String> tags = getTags();
|
||||
|
||||
if (tags != null && !tags.isEmpty()) {
|
||||
if (!tags.isEmpty()) {
|
||||
final var itemBinding = ItemMetadataTagsBinding.inflate(inflater, layout, false);
|
||||
|
||||
tags.stream().sorted(String.CASE_INSENSITIVE_ORDER).forEach(tag -> {
|
||||
|
|
|
@ -7,6 +7,7 @@ import android.view.LayoutInflater;
|
|||
import android.view.View;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
|
||||
|
@ -23,56 +24,43 @@ import icepick.State;
|
|||
public class DescriptionFragment extends BaseDescriptionFragment {
|
||||
|
||||
@State
|
||||
StreamInfo streamInfo = null;
|
||||
|
||||
public DescriptionFragment() {
|
||||
}
|
||||
StreamInfo streamInfo;
|
||||
|
||||
public DescriptionFragment(final StreamInfo streamInfo) {
|
||||
this.streamInfo = streamInfo;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
protected Description getDescription() {
|
||||
if (streamInfo == null) {
|
||||
return null;
|
||||
}
|
||||
return streamInfo.getDescription();
|
||||
public DescriptionFragment() {
|
||||
// keep empty constructor for IcePick when resuming fragment from memory
|
||||
}
|
||||
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
protected Description getDescription() {
|
||||
return streamInfo.getDescription();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected StreamingService getService() {
|
||||
if (streamInfo == null) {
|
||||
return null;
|
||||
}
|
||||
return streamInfo.getService();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getServiceId() {
|
||||
if (streamInfo == null) {
|
||||
return -1;
|
||||
}
|
||||
return streamInfo.getServiceId();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@NonNull
|
||||
@Override
|
||||
protected String getStreamUrl() {
|
||||
if (streamInfo == null) {
|
||||
return null;
|
||||
}
|
||||
return streamInfo.getUrl();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@NonNull
|
||||
@Override
|
||||
public List<String> getTags() {
|
||||
if (streamInfo == null) {
|
||||
return null;
|
||||
}
|
||||
return streamInfo.getTags();
|
||||
}
|
||||
|
||||
|
|
|
@ -72,7 +72,6 @@ import org.schabi.newpipe.error.ErrorUtil;
|
|||
import org.schabi.newpipe.error.ReCaptchaActivity;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
import org.schabi.newpipe.extractor.Image;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
||||
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
|
||||
|
@ -107,16 +106,17 @@ import org.schabi.newpipe.player.ui.VideoPlayerUi;
|
|||
import org.schabi.newpipe.util.Constants;
|
||||
import org.schabi.newpipe.util.DeviceUtils;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
import org.schabi.newpipe.util.InfoCache;
|
||||
import org.schabi.newpipe.util.ListHelper;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.PermissionHelper;
|
||||
import org.schabi.newpipe.util.image.PicassoHelper;
|
||||
import org.schabi.newpipe.util.PlayButtonHelper;
|
||||
import org.schabi.newpipe.util.StreamTypeUtil;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
import org.schabi.newpipe.util.external_communication.KoreUtils;
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||
import org.schabi.newpipe.util.PlayButtonHelper;
|
||||
import org.schabi.newpipe.util.image.PicassoHelper;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
|
@ -482,7 +482,7 @@ public final class VideoDetailFragment
|
|||
|
||||
// commit previous pending changes to database
|
||||
if (fragment instanceof LocalPlaylistFragment) {
|
||||
((LocalPlaylistFragment) fragment).commitChanges();
|
||||
((LocalPlaylistFragment) fragment).saveImmediate();
|
||||
} else if (fragment instanceof MainFragment) {
|
||||
((MainFragment) fragment).commitPlaylistTabs();
|
||||
}
|
||||
|
@ -1445,7 +1445,7 @@ public final class VideoDetailFragment
|
|||
super.showLoading();
|
||||
|
||||
//if data is already cached, transition from VISIBLE -> INVISIBLE -> VISIBLE is not required
|
||||
if (!ExtractorHelper.isCached(serviceId, url, InfoItem.InfoType.STREAM)) {
|
||||
if (!ExtractorHelper.isCached(serviceId, url, InfoCache.Type.STREAM)) {
|
||||
binding.detailContentRootHiding.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
|
||||
|
|
|
@ -2,12 +2,12 @@ package org.schabi.newpipe.fragments.list.channel;
|
|||
|
||||
import static org.schabi.newpipe.extractor.stream.StreamExtractor.UNKNOWN_SUBSCRIBER_COUNT;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
|
@ -26,14 +26,12 @@ public class ChannelAboutFragment extends BaseDescriptionFragment {
|
|||
@State
|
||||
protected ChannelInfo channelInfo;
|
||||
|
||||
public static ChannelAboutFragment getInstance(final ChannelInfo channelInfo) {
|
||||
final ChannelAboutFragment fragment = new ChannelAboutFragment();
|
||||
fragment.channelInfo = channelInfo;
|
||||
return fragment;
|
||||
ChannelAboutFragment(@NonNull final ChannelInfo channelInfo) {
|
||||
this.channelInfo = channelInfo;
|
||||
}
|
||||
|
||||
public ChannelAboutFragment() {
|
||||
super();
|
||||
// keep empty constructor for IcePick when resuming fragment from memory
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -45,26 +43,17 @@ public class ChannelAboutFragment extends BaseDescriptionFragment {
|
|||
@Nullable
|
||||
@Override
|
||||
protected Description getDescription() {
|
||||
if (channelInfo == null) {
|
||||
return null;
|
||||
}
|
||||
return new Description(channelInfo.getDescription(), Description.PLAIN_TEXT);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@NonNull
|
||||
@Override
|
||||
protected StreamingService getService() {
|
||||
if (channelInfo == null) {
|
||||
return null;
|
||||
}
|
||||
return channelInfo.getService();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getServiceId() {
|
||||
if (channelInfo == null) {
|
||||
return -1;
|
||||
}
|
||||
return channelInfo.getServiceId();
|
||||
}
|
||||
|
||||
|
@ -74,12 +63,9 @@ public class ChannelAboutFragment extends BaseDescriptionFragment {
|
|||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@NonNull
|
||||
@Override
|
||||
public List<String> getTags() {
|
||||
if (channelInfo == null) {
|
||||
return null;
|
||||
}
|
||||
return channelInfo.getTags();
|
||||
}
|
||||
|
||||
|
@ -93,10 +79,11 @@ public class ChannelAboutFragment extends BaseDescriptionFragment {
|
|||
return;
|
||||
}
|
||||
|
||||
final Context context = getContext();
|
||||
if (channelInfo.getSubscriberCount() != UNKNOWN_SUBSCRIBER_COUNT) {
|
||||
addMetadataItem(inflater, layout, false, R.string.metadata_subscribers,
|
||||
Localization.localizeNumber(context, channelInfo.getSubscriberCount()));
|
||||
Localization.localizeNumber(
|
||||
requireContext(),
|
||||
channelInfo.getSubscriberCount()));
|
||||
}
|
||||
|
||||
addImagesMetadataItem(inflater, layout, R.string.metadata_avatars,
|
||||
|
|
|
@ -474,7 +474,7 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
|||
if (ChannelTabHelper.showChannelTab(
|
||||
context, preferences, R.string.show_channel_tabs_about)) {
|
||||
tabAdapter.addFragment(
|
||||
ChannelAboutFragment.getInstance(currentInfo),
|
||||
new ChannelAboutFragment(currentInfo),
|
||||
context.getString(R.string.channel_tab_about));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,6 +30,7 @@ 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;
|
||||
|
||||
|
@ -38,7 +39,8 @@ public final class CommentRepliesFragment
|
|||
|
||||
public static final String TAG = CommentRepliesFragment.class.getSimpleName();
|
||||
|
||||
private CommentsInfoItem commentsInfoItem; // the comment to show replies of
|
||||
@State
|
||||
CommentsInfoItem commentsInfoItem; // the comment to show replies of
|
||||
private final CompositeDisposable disposables = new CompositeDisposable();
|
||||
|
||||
|
||||
|
|
|
@ -89,6 +89,9 @@ public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, Playl
|
|||
|
||||
private MenuItem playlistBookmarkButton;
|
||||
|
||||
private long streamCount;
|
||||
private long playlistOverallDurationSeconds;
|
||||
|
||||
public static PlaylistFragment getInstance(final int serviceId, final String url,
|
||||
final String name) {
|
||||
final PlaylistFragment instance = new PlaylistFragment();
|
||||
|
@ -277,6 +280,12 @@ public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, Playl
|
|||
animate(headerBinding.uploaderLayout, false, 200);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleNextItems(final ListExtractor.InfoItemsPage result) {
|
||||
super.handleNextItems(result);
|
||||
setStreamCountAndOverallDuration(result.getItems(), !result.hasNextPage());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleResult(@NonNull final PlaylistInfo result) {
|
||||
super.handleResult(result);
|
||||
|
@ -322,8 +331,8 @@ public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, Playl
|
|||
.into(headerBinding.uploaderAvatarView);
|
||||
}
|
||||
|
||||
headerBinding.playlistStreamCount.setText(Localization
|
||||
.localizeStreamCount(getContext(), result.getStreamCount()));
|
||||
streamCount = result.getStreamCount();
|
||||
setStreamCountAndOverallDuration(result.getRelatedItems(), !result.hasNextPage());
|
||||
|
||||
final Description description = result.getDescription();
|
||||
if (description != null && description != Description.EMPTY_DESCRIPTION
|
||||
|
@ -486,4 +495,20 @@ public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, Playl
|
|||
playlistBookmarkButton.setIcon(drawable);
|
||||
playlistBookmarkButton.setTitle(titleRes);
|
||||
}
|
||||
|
||||
private void setStreamCountAndOverallDuration(final List<StreamInfoItem> list,
|
||||
final boolean isDurationComplete) {
|
||||
if (activity != null && headerBinding != null) {
|
||||
playlistOverallDurationSeconds += list.stream()
|
||||
.mapToLong(x -> x.getDuration())
|
||||
.sum();
|
||||
headerBinding.playlistStreamCount.setText(
|
||||
Localization.concatenateStrings(
|
||||
Localization.localizeStreamCount(activity, streamCount),
|
||||
Localization.getDurationString(playlistOverallDurationSeconds,
|
||||
isDurationComplete, true))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package org.schabi.newpipe.fragments.list.search;
|
||||
|
||||
import static androidx.recyclerview.widget.ItemTouchHelper.Callback.makeMovementFlags;
|
||||
import static org.schabi.newpipe.extractor.utils.Utils.isBlank;
|
||||
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
||||
import static org.schabi.newpipe.util.ExtractorHelper.showMetaInfoInTextView;
|
||||
import static java.util.Arrays.asList;
|
||||
|
@ -389,7 +390,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||
@Override
|
||||
public void onSaveInstanceState(@NonNull final Bundle bundle) {
|
||||
searchString = searchEditText != null
|
||||
? searchEditText.getText().toString()
|
||||
? getSearchEditString().trim()
|
||||
: searchString;
|
||||
super.onSaveInstanceState(bundle);
|
||||
}
|
||||
|
@ -400,11 +401,11 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||
|
||||
@Override
|
||||
public void reloadContent() {
|
||||
if (!TextUtils.isEmpty(searchString)
|
||||
|| (searchEditText != null && !TextUtils.isEmpty(searchEditText.getText()))) {
|
||||
if (!TextUtils.isEmpty(searchString) || (searchEditText != null
|
||||
&& !isSearchEditBlank())) {
|
||||
search(!TextUtils.isEmpty(searchString)
|
||||
? searchString
|
||||
: searchEditText.getText().toString(), this.contentFilter, "");
|
||||
: getSearchEditString(), this.contentFilter, "");
|
||||
} else {
|
||||
if (searchEditText != null) {
|
||||
searchEditText.setText("");
|
||||
|
@ -498,7 +499,8 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||
}
|
||||
searchEditText.setText(searchString);
|
||||
|
||||
if (TextUtils.isEmpty(searchString) || TextUtils.isEmpty(searchEditText.getText())) {
|
||||
if (TextUtils.isEmpty(searchString)
|
||||
|| isSearchEditBlank()) {
|
||||
searchToolbarContainer.setTranslationX(100);
|
||||
searchToolbarContainer.setAlpha(0.0f);
|
||||
searchToolbarContainer.setVisibility(View.VISIBLE);
|
||||
|
@ -522,7 +524,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||
if (DEBUG) {
|
||||
Log.d(TAG, "onClick() called with: v = [" + v + "]");
|
||||
}
|
||||
if (TextUtils.isEmpty(searchEditText.getText())) {
|
||||
if (isSearchEditBlank()) {
|
||||
NavigationHelper.gotoMainFragment(getFM());
|
||||
return;
|
||||
}
|
||||
|
@ -603,7 +605,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||
s.removeSpan(span);
|
||||
}
|
||||
|
||||
final String newText = searchEditText.getText().toString();
|
||||
final String newText = getSearchEditString().trim();
|
||||
suggestionPublisher.onNext(newText);
|
||||
}
|
||||
};
|
||||
|
@ -619,7 +621,8 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||
} else if (event != null
|
||||
&& (event.getKeyCode() == KeyEvent.KEYCODE_ENTER
|
||||
|| event.getAction() == EditorInfo.IME_ACTION_SEARCH)) {
|
||||
search(searchEditText.getText().toString(), new String[0], "");
|
||||
searchEditText.setText(getSearchEditString().trim());
|
||||
search(getSearchEditString(), new String[0], "");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
@ -694,7 +697,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
howManyDeleted -> suggestionPublisher
|
||||
.onNext(searchEditText.getText().toString()),
|
||||
.onNext(getSearchEditString()),
|
||||
throwable -> showSnackBarError(new ErrorInfo(throwable,
|
||||
UserAction.DELETE_FROM_HISTORY,
|
||||
"Deleting item failed")));
|
||||
|
@ -723,9 +726,9 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||
.getRelatedSearches(query, similarQueryLimit, 25)
|
||||
.toObservable()
|
||||
.map(searchHistoryEntries ->
|
||||
searchHistoryEntries.stream()
|
||||
.map(entry -> new SuggestionItem(true, entry))
|
||||
.collect(Collectors.toList()));
|
||||
searchHistoryEntries.stream()
|
||||
.map(entry -> new SuggestionItem(true, entry))
|
||||
.collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
private Observable<List<SuggestionItem>> getRemoteSuggestionsObservable(final String query) {
|
||||
|
@ -792,12 +795,12 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||
} else if (listNotification.isOnError()
|
||||
&& listNotification.getError() != null
|
||||
&& !ExceptionUtils.isInterruptedCaused(
|
||||
listNotification.getError())) {
|
||||
listNotification.getError())) {
|
||||
showSnackBarError(new ErrorInfo(listNotification.getError(),
|
||||
UserAction.GET_SUGGESTIONS, searchString, serviceId));
|
||||
}
|
||||
}, throwable -> showSnackBarError(new ErrorInfo(
|
||||
throwable, UserAction.GET_SUGGESTIONS, searchString, serviceId)));
|
||||
throwable, UserAction.GET_SUGGESTIONS, searchString, serviceId)));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -805,7 +808,13 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||
// no-op
|
||||
}
|
||||
|
||||
private void search(final String theSearchString,
|
||||
/**
|
||||
* Perform a search.
|
||||
* @param theSearchString the trimmed search string
|
||||
* @param theContentFilter the content filter to use. FIXME: unused param
|
||||
* @param theSortFilter FIXME: unused param
|
||||
*/
|
||||
private void search(@NonNull final String theSearchString,
|
||||
final String[] theContentFilter,
|
||||
final String theSortFilter) {
|
||||
if (DEBUG) {
|
||||
|
@ -815,25 +824,26 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||
return;
|
||||
}
|
||||
|
||||
// Check if theSearchString is a URL which can be opened by NewPipe directly
|
||||
// and open it if possible.
|
||||
try {
|
||||
final StreamingService streamingService = NewPipe.getServiceByUrl(theSearchString);
|
||||
if (streamingService != null) {
|
||||
showLoading();
|
||||
disposables.add(Observable
|
||||
.fromCallable(() -> NavigationHelper.getIntentByLink(activity,
|
||||
streamingService, theSearchString))
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(intent -> {
|
||||
getFM().popBackStackImmediate();
|
||||
activity.startActivity(intent);
|
||||
}, throwable -> showTextError(getString(R.string.unsupported_url))));
|
||||
return;
|
||||
}
|
||||
showLoading();
|
||||
disposables.add(Observable
|
||||
.fromCallable(() -> NavigationHelper.getIntentByLink(activity,
|
||||
streamingService, theSearchString))
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(intent -> {
|
||||
getFM().popBackStackImmediate();
|
||||
activity.startActivity(intent);
|
||||
}, throwable -> showTextError(getString(R.string.unsupported_url))));
|
||||
return;
|
||||
} catch (final Exception ignored) {
|
||||
// Exception occurred, it's not a url
|
||||
}
|
||||
|
||||
// prepare search
|
||||
lastSearchedString = this.searchString;
|
||||
this.searchString = theSearchString;
|
||||
infoListAdapter.clearStreamItemList();
|
||||
|
@ -842,13 +852,17 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||
searchBinding.searchMetaInfoSeparator, disposables);
|
||||
hideKeyboardSearch();
|
||||
|
||||
// store search query if search history is enabled
|
||||
disposables.add(historyRecordManager.onSearched(serviceId, theSearchString)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
ignored -> { },
|
||||
ignored -> {
|
||||
},
|
||||
throwable -> showSnackBarError(new ErrorInfo(throwable, UserAction.SEARCHED,
|
||||
theSearchString, serviceId))
|
||||
));
|
||||
|
||||
// load search results
|
||||
suggestionPublisher.onNext(theSearchString);
|
||||
startLoading(false);
|
||||
}
|
||||
|
@ -938,6 +952,14 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||
sortFilter = theSortFilter;
|
||||
}
|
||||
|
||||
private String getSearchEditString() {
|
||||
return searchEditText.getText().toString();
|
||||
}
|
||||
|
||||
private boolean isSearchEditBlank() {
|
||||
return isBlank(getSearchEditString());
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Suggestion Results
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
@ -979,6 +1001,9 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||
}
|
||||
|
||||
searchSuggestion = result.getSearchSuggestion();
|
||||
if (searchSuggestion != null) {
|
||||
searchSuggestion = searchSuggestion.trim();
|
||||
}
|
||||
isCorrectedSearch = result.isCorrectedSearch();
|
||||
|
||||
// List<MetaInfo> cannot be bundled without creating some containers
|
||||
|
@ -1080,7 +1105,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
howManyDeleted -> suggestionPublisher
|
||||
.onNext(searchEditText.getText().toString()),
|
||||
.onNext(getSearchEditString()),
|
||||
throwable -> showSnackBarError(new ErrorInfo(throwable,
|
||||
UserAction.DELETE_FROM_HISTORY, "Deleting item failed")));
|
||||
disposables.add(onDelete);
|
||||
|
|
|
@ -14,6 +14,7 @@ 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;
|
||||
|
@ -24,6 +25,7 @@ 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;
|
||||
|
@ -73,10 +75,12 @@ 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;
|
||||
|
@ -87,6 +91,7 @@ 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);
|
||||
|
@ -180,6 +185,10 @@ 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;
|
||||
|
@ -257,7 +266,9 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
|
|||
final LocalItem item = localItems.get(position);
|
||||
switch (item.getLocalItemType()) {
|
||||
case PLAYLIST_LOCAL_ITEM:
|
||||
if (itemViewMode == ItemViewMode.CARD) {
|
||||
if (useItemHandle) {
|
||||
return LOCAL_BOOKMARK_PLAYLIST_HOLDER_TYPE;
|
||||
} else if (itemViewMode == ItemViewMode.CARD) {
|
||||
return LOCAL_PLAYLIST_CARD_HOLDER_TYPE;
|
||||
} else if (itemViewMode == ItemViewMode.GRID) {
|
||||
return LOCAL_PLAYLIST_GRID_HOLDER_TYPE;
|
||||
|
@ -265,7 +276,9 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
|
|||
return LOCAL_PLAYLIST_HOLDER_TYPE;
|
||||
}
|
||||
case PLAYLIST_REMOTE_ITEM:
|
||||
if (itemViewMode == ItemViewMode.CARD) {
|
||||
if (useItemHandle) {
|
||||
return REMOTE_BOOKMARK_PLAYLIST_HOLDER_TYPE;
|
||||
} else if (itemViewMode == ItemViewMode.CARD) {
|
||||
return REMOTE_PLAYLIST_CARD_HOLDER_TYPE;
|
||||
} else if (itemViewMode == ItemViewMode.GRID) {
|
||||
return REMOTE_PLAYLIST_GRID_HOLDER_TYPE;
|
||||
|
@ -314,12 +327,16 @@ 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:
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
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;
|
||||
|
@ -13,6 +16,8 @@ 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;
|
||||
|
@ -27,29 +32,45 @@ 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> {
|
||||
public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistLocalItem>, Void>
|
||||
implements DebounceSavable {
|
||||
|
||||
private static final int MINIMUM_INITIAL_DRAG_VELOCITY = 12;
|
||||
@State
|
||||
protected Parcelable itemsListState;
|
||||
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
|
||||
|
@ -65,6 +86,11 @@ 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
|
||||
|
@ -91,10 +117,20 @@ 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) {
|
||||
|
@ -102,7 +138,7 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
|||
|
||||
if (selectedItem instanceof PlaylistMetadataEntry) {
|
||||
final PlaylistMetadataEntry entry = ((PlaylistMetadataEntry) selectedItem);
|
||||
NavigationHelper.openLocalPlaylistFragment(fragmentManager, entry.uid,
|
||||
NavigationHelper.openLocalPlaylistFragment(fragmentManager, entry.getUid(),
|
||||
entry.name);
|
||||
|
||||
} else if (selectedItem instanceof PlaylistRemoteEntity) {
|
||||
|
@ -123,6 +159,14 @@ 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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -134,8 +178,13 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
|||
public void startLoading(final boolean forceLoad) {
|
||||
super.startLoading(forceLoad);
|
||||
|
||||
Flowable.combineLatest(localPlaylistManager.getPlaylists(),
|
||||
remotePlaylistManager.getPlaylists(), PlaylistLocalItem::merge)
|
||||
if (debounceSaver != null) {
|
||||
disposables.add(debounceSaver.getDebouncedSaver());
|
||||
debounceSaver.setNoChangesToSave();
|
||||
}
|
||||
isLoadingComplete.set(false);
|
||||
|
||||
getMergedOrderedPlaylists(localPlaylistManager, remotePlaylistManager)
|
||||
.onBackpressureLatest()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(getPlaylistsSubscriber());
|
||||
|
@ -149,6 +198,9 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
|||
public void onPause() {
|
||||
super.onPause();
|
||||
itemsListState = itemsList.getLayoutManager().onSaveInstanceState();
|
||||
|
||||
// Save on exit
|
||||
saveImmediate();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -163,19 +215,27 @@ 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;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
@ -183,10 +243,12 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
|||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private Subscriber<List<PlaylistLocalItem>> getPlaylistsSubscriber() {
|
||||
return new Subscriber<List<PlaylistLocalItem>>() {
|
||||
return new Subscriber<>() {
|
||||
@Override
|
||||
public void onSubscribe(final Subscription s) {
|
||||
showLoading();
|
||||
isLoadingComplete.set(false);
|
||||
|
||||
if (databaseSubscription != null) {
|
||||
databaseSubscription.cancel();
|
||||
}
|
||||
|
@ -196,7 +258,10 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
|||
|
||||
@Override
|
||||
public void onNext(final List<PlaylistLocalItem> subscriptions) {
|
||||
handleResult(subscriptions);
|
||||
if (debounceSaver == null || !debounceSaver.getIsModified()) {
|
||||
handleResult(subscriptions);
|
||||
isLoadingComplete.set(true);
|
||||
}
|
||||
if (databaseSubscription != null) {
|
||||
databaseSubscription.request(1);
|
||||
}
|
||||
|
@ -209,7 +274,8 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onComplete() { }
|
||||
public void onComplete() {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -244,12 +310,183 @@ 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(), remotePlaylistManager.deletePlaylist(item.getUid()));
|
||||
showDeleteDialog(item.getName(), item);
|
||||
}
|
||||
|
||||
private void showLocalDialog(final PlaylistMetadataEntry selectedItem) {
|
||||
|
@ -257,7 +494,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.uid);
|
||||
.getIsPlaylistThumbnailPermanent(selectedItem.getUid());
|
||||
|
||||
final ArrayList<String> items = new ArrayList<>();
|
||||
items.add(rename);
|
||||
|
@ -270,13 +507,12 @@ 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,
|
||||
localPlaylistManager.deletePlaylist(selectedItem.uid));
|
||||
showDeleteDialog(selectedItem.name, selectedItem);
|
||||
} else if (isThumbnailPermanent && items.get(index).equals(unsetThumbnail)) {
|
||||
final long thumbnailStreamId = localPlaylistManager
|
||||
.getAutomaticPlaylistThumbnailStreamId(selectedItem.uid);
|
||||
.getAutomaticPlaylistThumbnailStreamId(selectedItem.getUid());
|
||||
localPlaylistManager
|
||||
.changePlaylistThumbnail(selectedItem.uid, thumbnailStreamId, false)
|
||||
.changePlaylistThumbnail(selectedItem.getUid(), thumbnailStreamId, false)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe();
|
||||
}
|
||||
|
@ -298,13 +534,13 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
|||
.setView(dialogBinding.getRoot())
|
||||
.setPositiveButton(R.string.rename_playlist, (dialog, which) ->
|
||||
changeLocalPlaylistName(
|
||||
selectedItem.uid,
|
||||
selectedItem.getUid(),
|
||||
dialogBinding.dialogEditText.getText().toString()))
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void showDeleteDialog(final String name, final Single<Integer> deleteReactor) {
|
||||
private void showDeleteDialog(final String name, final PlaylistLocalItem item) {
|
||||
if (activity == null || disposables == null) {
|
||||
return;
|
||||
}
|
||||
|
@ -313,35 +549,8 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
|||
.setTitle(name)
|
||||
.setMessage(R.string.delete_playlist_prompt)
|
||||
.setCancelable(true)
|
||||
.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")))))
|
||||
.setPositiveButton(R.string.delete, (dialog, i) -> deleteItem(item))
|
||||
.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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,95 @@
|
|||
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);
|
||||
}
|
||||
}
|
|
@ -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.uid, streams)
|
||||
playlistDisposables.add(manager.appendToPlaylist(playlist.getUid(), streams)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(ignored -> {
|
||||
successToast.show();
|
||||
|
||||
if (playlist.thumbnailUrl.equals(PlaylistEntity.DEFAULT_THUMBNAIL)) {
|
||||
playlistDisposables.add(manager
|
||||
.changePlaylistThumbnail(playlist.uid, streams.get(0).getUid(),
|
||||
.changePlaylistThumbnail(playlist.getUid(), streams.get(0).getUid(),
|
||||
false)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(ignore -> successToast.show()));
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
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;
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
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;
|
||||
};
|
||||
}
|
||||
}
|
|
@ -14,6 +14,7 @@ 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);
|
||||
|
|
|
@ -49,6 +49,8 @@ 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;
|
||||
|
@ -58,7 +60,6 @@ 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;
|
||||
|
||||
|
@ -68,12 +69,10 @@ 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 {
|
||||
/** Save the list 10 seconds after the last change occurred. */
|
||||
private static final long SAVE_DEBOUNCE_MILLIS = 10000;
|
||||
implements PlaylistControlViewHolder, DebounceSavable {
|
||||
|
||||
private static final int MINIMUM_INITIAL_DRAG_VELOCITY = 12;
|
||||
@State
|
||||
protected Long playlistId;
|
||||
|
@ -90,13 +89,12 @@ 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;
|
||||
/** Whether the playlist has been modified (e.g. items reordered or deleted) */
|
||||
private AtomicBoolean isModified;
|
||||
/** Used to debounce saving playlist edits to disk. */
|
||||
private DebounceSaver debounceSaver;
|
||||
/** Flag to prevent simultaneous rewrites of the playlist. */
|
||||
private boolean isRewritingPlaylist = false;
|
||||
|
||||
|
@ -121,12 +119,11 @@ 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();
|
||||
isModified = new AtomicBoolean();
|
||||
debounceSaver = new DebounceSaver(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -166,17 +163,6 @@ 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();
|
||||
|
@ -243,10 +229,13 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
|||
if (disposables != null) {
|
||||
disposables.clear();
|
||||
}
|
||||
disposables.add(getDebouncedSaver());
|
||||
|
||||
if (debounceSaver != null) {
|
||||
disposables.add(debounceSaver.getDebouncedSaver());
|
||||
debounceSaver.setNoChangesToSave();
|
||||
}
|
||||
|
||||
isLoadingComplete.set(false);
|
||||
isModified.set(false);
|
||||
|
||||
playlistManager.getPlaylistStreams(playlistId)
|
||||
.onBackpressureLatest()
|
||||
|
@ -304,8 +293,8 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
|||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (debouncedSaveSignal != null) {
|
||||
debouncedSaveSignal.onComplete();
|
||||
if (debounceSaver != null) {
|
||||
debounceSaver.getDebouncedSaveSignal().onComplete();
|
||||
}
|
||||
if (disposables != null) {
|
||||
disposables.dispose();
|
||||
|
@ -314,12 +303,11 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
|||
tabsPagerAdapter.getLocalPlaylistFragments().remove(this);
|
||||
}
|
||||
|
||||
debouncedSaveSignal = null;
|
||||
debounceSaver = null;
|
||||
playlistManager = null;
|
||||
disposables = null;
|
||||
|
||||
isLoadingComplete = null;
|
||||
isModified = null;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
@ -343,7 +331,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 (isModified == null || !isModified.get()) {
|
||||
if (debounceSaver == null || !debounceSaver.getIsModified()) {
|
||||
handleResult(streams);
|
||||
isLoadingComplete.set(true);
|
||||
}
|
||||
|
@ -495,14 +483,14 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
|||
|
||||
itemListAdapter.clearStreamItemList();
|
||||
itemListAdapter.addItems(itemsToKeep);
|
||||
saveChanges();
|
||||
debounceSaver.setHasChangesToSave();
|
||||
|
||||
if (thumbnailVideoRemoved) {
|
||||
updateThumbnailUrl();
|
||||
}
|
||||
|
||||
final long videoCount = itemListAdapter.getItemsList().size();
|
||||
setVideoCount(videoCount);
|
||||
setStreamCountAndOverallDuration(itemListAdapter.getItemsList());
|
||||
if (videoCount == 0) {
|
||||
showEmptyState();
|
||||
}
|
||||
|
@ -532,7 +520,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
|||
itemsList.getLayoutManager().onRestoreInstanceState(itemsListState);
|
||||
itemsListState = null;
|
||||
}
|
||||
setVideoCount(itemListAdapter.getItemsList().size());
|
||||
setStreamCountAndOverallDuration(itemListAdapter.getItemsList());
|
||||
|
||||
PlayButtonHelper.initPlaylistControlClickListener(activity, playlistControlBinding, this);
|
||||
|
||||
|
@ -665,8 +653,8 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
|||
.subscribe(itemsToKeep -> {
|
||||
itemListAdapter.clearStreamItemList();
|
||||
itemListAdapter.addItems(itemsToKeep);
|
||||
setVideoCount(itemListAdapter.getItemsList().size());
|
||||
saveChanges();
|
||||
setStreamCountAndOverallDuration(itemListAdapter.getItemsList());
|
||||
debounceSaver.setHasChangesToSave();
|
||||
|
||||
hideLoading();
|
||||
isRewritingPlaylist = false;
|
||||
|
@ -684,42 +672,24 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
|||
updateThumbnailUrl();
|
||||
}
|
||||
|
||||
setVideoCount(itemListAdapter.getItemsList().size());
|
||||
saveChanges();
|
||||
setStreamCountAndOverallDuration(itemListAdapter.getItemsList());
|
||||
debounceSaver.setHasChangesToSave();
|
||||
}
|
||||
|
||||
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() {
|
||||
/**
|
||||
* <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() {
|
||||
if (playlistManager == null || itemListAdapter == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// List must be loaded and modified in order to save
|
||||
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 + "]");
|
||||
if (isLoadingComplete == null || debounceSaver == null
|
||||
|| !isLoadingComplete.get() || !debounceSaver.getIsModified()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -740,8 +710,8 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
|||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
() -> {
|
||||
if (isModified != null) {
|
||||
isModified.set(false);
|
||||
if (debounceSaver != null) {
|
||||
debounceSaver.setNoChangesToSave();
|
||||
}
|
||||
},
|
||||
throwable -> showError(new ErrorInfo(throwable,
|
||||
|
@ -784,7 +754,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
|||
final int targetIndex = target.getBindingAdapterPosition();
|
||||
final boolean isSwapped = itemListAdapter.swapItems(sourceIndex, targetIndex);
|
||||
if (isSwapped) {
|
||||
saveChanges();
|
||||
debounceSaver.setHasChangesToSave();
|
||||
}
|
||||
return isSwapped;
|
||||
}
|
||||
|
@ -855,10 +825,21 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
|||
this.name = !TextUtils.isEmpty(title) ? title : "";
|
||||
}
|
||||
|
||||
private void setVideoCount(final long count) {
|
||||
private void setStreamCountAndOverallDuration(final ArrayList<LocalItem> itemsList) {
|
||||
if (activity != null && headerBinding != null) {
|
||||
headerBinding.playlistStreamCount.setText(Localization
|
||||
.localizeStreamCount(activity, count));
|
||||
final long streamCount = itemsList.size();
|
||||
final long playlistOverallDurationSeconds = itemsList.stream()
|
||||
.filter(PlaylistStreamEntry.class::isInstance)
|
||||
.map(PlaylistStreamEntry.class::cast)
|
||||
.map(PlaylistStreamEntry::getStreamEntity)
|
||||
.mapToLong(StreamEntity::getDuration)
|
||||
.sum();
|
||||
headerBinding.playlistStreamCount.setText(
|
||||
Localization.concatenateStrings(
|
||||
Localization.localizeStreamCount(activity, streamCount),
|
||||
Localization.getDurationString(playlistOverallDurationSeconds,
|
||||
true, true))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -19,7 +19,6 @@ 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 {
|
||||
|
@ -43,10 +42,13 @@ 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));
|
||||
streamIds.get(0), -1);
|
||||
|
||||
return insertJoinEntities(playlistTable.insert(newPlaylist),
|
||||
streamIds, 0);
|
||||
|
@ -89,8 +91,20 @@ public class LocalPlaylistManager {
|
|||
})).subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
public Flowable<List<PlaylistMetadataEntry>> getPlaylists() {
|
||||
return playlistStreamTable.getPlaylistMetadata().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<PlaylistStreamEntry>> getDistinctPlaylistStreams(final long playlistId) {
|
||||
|
@ -110,13 +124,12 @@ public class LocalPlaylistManager {
|
|||
.subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
public Flowable<List<PlaylistStreamEntry>> getPlaylistStreams(final long playlistId) {
|
||||
return playlistStreamTable.getOrderedStreamsOf(playlistId).subscribeOn(Schedulers.io());
|
||||
public Flowable<List<PlaylistMetadataEntry>> getPlaylists() {
|
||||
return playlistStreamTable.getPlaylistMetadata().subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
public Single<Integer> deletePlaylist(final long playlistId) {
|
||||
return Single.fromCallable(() -> playlistTable.deletePlaylist(playlistId))
|
||||
.subscribeOn(Schedulers.io());
|
||||
public Flowable<List<PlaylistStreamEntry>> getPlaylistStreams(final long playlistId) {
|
||||
return playlistStreamTable.getOrderedStreamsOf(playlistId).subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
public Maybe<Integer> renamePlaylist(final long playlistId, final String name) {
|
||||
|
|
|
@ -7,20 +7,23 @@ 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.getAll().subscribeOn(Schedulers.io());
|
||||
return playlistRemoteTable.getPlaylists().subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
public Flowable<List<PlaylistRemoteEntity>> getPlaylist(final PlaylistInfo info) {
|
||||
|
@ -33,6 +36,18 @@ 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);
|
||||
|
|
|
@ -100,7 +100,9 @@ class SubscriptionManager(context: Context) {
|
|||
val subscriptionEntity = subscriptionTable.getSubscription(info.uid)
|
||||
|
||||
subscriptionEntity.name = info.name
|
||||
subscriptionEntity.avatarUrl = info.avatarUrl
|
||||
|
||||
// some services do not provide an avatar URL
|
||||
info.avatarUrl?.let { subscriptionEntity.avatarUrl = it }
|
||||
|
||||
// these two fields are null if the feed info was fetched using the fast feed method
|
||||
info.description?.let { subscriptionEntity.description = it }
|
||||
|
|
|
@ -0,0 +1,301 @@
|
|||
package org.schabi.newpipe.settings;
|
||||
|
||||
import static org.schabi.newpipe.extractor.utils.Utils.isBlank;
|
||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.activity.result.ActivityResult;
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
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;
|
||||
import org.schabi.newpipe.util.ZipHelper;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
|
||||
public class BackupRestoreSettingsFragment extends BasePreferenceFragment {
|
||||
|
||||
private static final String ZIP_MIME_TYPE = "application/zip";
|
||||
|
||||
private final SimpleDateFormat exportDateFormat =
|
||||
new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US);
|
||||
private ImportExportManager manager;
|
||||
private String importExportDataPathKey;
|
||||
private final ActivityResultLauncher<Intent> requestImportPathLauncher =
|
||||
registerForActivityResult(new ActivityResultContracts.StartActivityForResult(),
|
||||
this::requestImportPathResult);
|
||||
private final ActivityResultLauncher<Intent> requestExportPathLauncher =
|
||||
registerForActivityResult(new ActivityResultContracts.StartActivityForResult(),
|
||||
this::requestExportPathResult);
|
||||
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(@Nullable final Bundle savedInstanceState,
|
||||
@Nullable final String rootKey) {
|
||||
final File homeDir = ContextCompat.getDataDir(requireContext());
|
||||
Objects.requireNonNull(homeDir);
|
||||
manager = new ImportExportManager(new BackupFileLocator(homeDir));
|
||||
|
||||
importExportDataPathKey = getString(R.string.import_export_data_path);
|
||||
|
||||
|
||||
addPreferencesFromResourceRegistry();
|
||||
|
||||
final Preference importDataPreference = requirePreference(R.string.import_data);
|
||||
importDataPreference.setOnPreferenceClickListener((Preference p) -> {
|
||||
NoFileManagerSafeGuard.launchSafe(
|
||||
requestImportPathLauncher,
|
||||
StoredFileHelper.getPicker(requireContext(),
|
||||
ZIP_MIME_TYPE, getImportExportDataUri()),
|
||||
TAG,
|
||||
getContext()
|
||||
);
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
final Preference exportDataPreference = requirePreference(R.string.export_data);
|
||||
exportDataPreference.setOnPreferenceClickListener((final Preference p) -> {
|
||||
NoFileManagerSafeGuard.launchSafe(
|
||||
requestExportPathLauncher,
|
||||
StoredFileHelper.getNewPicker(requireContext(),
|
||||
"NewPipeData-" + exportDateFormat.format(new Date()) + ".zip",
|
||||
ZIP_MIME_TYPE, getImportExportDataUri()),
|
||||
TAG,
|
||||
getContext()
|
||||
);
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
final Preference resetSettings = findPreference(getString(R.string.reset_settings));
|
||||
// Resets all settings by deleting shared preference and restarting the app
|
||||
// A dialogue will pop up to confirm if user intends to reset all settings
|
||||
assert resetSettings != null;
|
||||
resetSettings.setOnPreferenceClickListener(preference -> {
|
||||
// Show Alert Dialogue
|
||||
final AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
|
||||
builder.setMessage(R.string.reset_all_settings);
|
||||
builder.setCancelable(true);
|
||||
builder.setPositiveButton(R.string.ok, (dialogInterface, i) -> {
|
||||
// Deletes all shared preferences xml files.
|
||||
final SharedPreferences sharedPreferences =
|
||||
PreferenceManager.getDefaultSharedPreferences(requireContext());
|
||||
sharedPreferences.edit().clear().apply();
|
||||
// Restarts the app
|
||||
if (getActivity() == null) {
|
||||
return;
|
||||
}
|
||||
NavigationHelper.restartApp(getActivity());
|
||||
});
|
||||
builder.setNegativeButton(R.string.cancel, (dialogInterface, i) -> {
|
||||
});
|
||||
final AlertDialog alertDialog = builder.create();
|
||||
alertDialog.show();
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private void requestExportPathResult(final ActivityResult result) {
|
||||
assureCorrectAppLanguage(requireContext());
|
||||
if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
|
||||
// will be saved only on success
|
||||
final Uri lastExportDataUri = result.getData().getData();
|
||||
|
||||
final StoredFileHelper file = new StoredFileHelper(
|
||||
requireContext(), result.getData().getData(), ZIP_MIME_TYPE);
|
||||
|
||||
exportDatabase(file, lastExportDataUri);
|
||||
}
|
||||
}
|
||||
|
||||
private void requestImportPathResult(final ActivityResult result) {
|
||||
assureCorrectAppLanguage(requireContext());
|
||||
if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
|
||||
// will be saved only on success
|
||||
final Uri lastImportDataUri = result.getData().getData();
|
||||
|
||||
final StoredFileHelper file = new StoredFileHelper(
|
||||
requireContext(), result.getData().getData(), ZIP_MIME_TYPE);
|
||||
|
||||
new androidx.appcompat.app.AlertDialog.Builder(requireActivity())
|
||||
.setMessage(R.string.override_current_data)
|
||||
.setPositiveButton(R.string.ok, (d, id) ->
|
||||
importDatabase(file, lastImportDataUri))
|
||||
.setNegativeButton(R.string.cancel, (d, id) ->
|
||||
d.cancel())
|
||||
.show();
|
||||
}
|
||||
}
|
||||
|
||||
private void exportDatabase(final StoredFileHelper file, final Uri exportDataUri) {
|
||||
try {
|
||||
//checkpoint before export
|
||||
NewPipeDatabase.checkpoint();
|
||||
|
||||
final SharedPreferences preferences = PreferenceManager
|
||||
.getDefaultSharedPreferences(requireContext());
|
||||
manager.exportDatabase(preferences, file);
|
||||
|
||||
saveLastImportExportDataUri(exportDataUri); // save export path only on success
|
||||
Toast.makeText(requireContext(), R.string.export_complete_toast, Toast.LENGTH_SHORT)
|
||||
.show();
|
||||
} catch (final Exception e) {
|
||||
showErrorSnackbar(e, "Exporting database and settings");
|
||||
}
|
||||
}
|
||||
|
||||
private void importDatabase(final StoredFileHelper file, final Uri importDataUri) {
|
||||
// check if file is supported
|
||||
if (!ZipHelper.isValidZipFile(file)) {
|
||||
Toast.makeText(requireContext(), R.string.no_valid_zip_file, Toast.LENGTH_SHORT)
|
||||
.show();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!manager.ensureDbDirectoryExists()) {
|
||||
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)
|
||||
.show();
|
||||
}
|
||||
|
||||
// if settings file exist, ask if it should be imported.
|
||||
final boolean hasJsonPrefs = manager.exportHasJsonPrefs(file);
|
||||
if (hasJsonPrefs || manager.exportHasSerializedPrefs(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);
|
||||
})
|
||||
.setPositiveButton(R.string.ok, (dialog, which) -> {
|
||||
dialog.dismiss();
|
||||
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;
|
||||
}
|
||||
cleanImport(context, prefs);
|
||||
finishImport(importDataUri);
|
||||
})
|
||||
.show();
|
||||
} else {
|
||||
finishImport(importDataUri);
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
showErrorSnackbar(e, "Importing database and settings");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove settings that are not supposed to be imported on different devices
|
||||
* and reset them to default values.
|
||||
* @param context the context used for the import
|
||||
* @param prefs the preferences used while running the import
|
||||
*/
|
||||
private void cleanImport(@NonNull final Context context,
|
||||
@NonNull final SharedPreferences prefs) {
|
||||
// Check if media tunnelling needs to be disabled automatically,
|
||||
// if it was disabled automatically in the imported preferences.
|
||||
final String tunnelingKey = context.getString(R.string.disable_media_tunneling_key);
|
||||
final String automaticTunnelingKey =
|
||||
context.getString(R.string.disabled_media_tunneling_automatically_key);
|
||||
// R.string.disable_media_tunneling_key should always be true
|
||||
// if R.string.disabled_media_tunneling_automatically_key equals 1,
|
||||
// but we double check here just to be sure and to avoid regressions
|
||||
// caused by possible later modification of the media tunneling functionality.
|
||||
// R.string.disabled_media_tunneling_automatically_key == 0:
|
||||
// automatic value overridden by user in settings
|
||||
// R.string.disabled_media_tunneling_automatically_key == -1: not set
|
||||
final boolean wasMediaTunnelingDisabledAutomatically =
|
||||
prefs.getInt(automaticTunnelingKey, -1) == 1
|
||||
&& prefs.getBoolean(tunnelingKey, false);
|
||||
if (wasMediaTunnelingDisabledAutomatically) {
|
||||
prefs.edit()
|
||||
.putInt(automaticTunnelingKey, -1)
|
||||
.putBoolean(tunnelingKey, false)
|
||||
.apply();
|
||||
NewPipeSettings.setMediaTunneling(context);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save import path and restart app.
|
||||
*
|
||||
* @param importDataUri The import path to save
|
||||
*/
|
||||
private void finishImport(final Uri importDataUri) {
|
||||
// save import path only on success
|
||||
saveLastImportExportDataUri(importDataUri);
|
||||
// restart app to properly load db
|
||||
NavigationHelper.restartApp(requireActivity());
|
||||
}
|
||||
|
||||
private Uri getImportExportDataUri() {
|
||||
final String path = defaultPreferences.getString(importExportDataPathKey, null);
|
||||
return isBlank(path) ? null : Uri.parse(path);
|
||||
}
|
||||
|
||||
private void saveLastImportExportDataUri(final Uri importExportDataUri) {
|
||||
final SharedPreferences.Editor editor = defaultPreferences.edit()
|
||||
.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)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,106 +1,36 @@
|
|||
package org.schabi.newpipe.settings;
|
||||
|
||||
import static org.schabi.newpipe.extractor.utils.Utils.isBlank;
|
||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.activity.result.ActivityResult;
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import org.schabi.newpipe.DownloaderImpl;
|
||||
import org.schabi.newpipe.NewPipeDatabase;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.error.ErrorUtil;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.localization.ContentCountry;
|
||||
import org.schabi.newpipe.extractor.localization.Localization;
|
||||
import org.schabi.newpipe.streams.io.NoFileManagerSafeGuard;
|
||||
import org.schabi.newpipe.streams.io.StoredFileHelper;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.image.ImageStrategy;
|
||||
import org.schabi.newpipe.util.image.PicassoHelper;
|
||||
import org.schabi.newpipe.util.ZipHelper;
|
||||
import org.schabi.newpipe.util.image.PreferredImageQuality;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
|
||||
public class ContentSettingsFragment extends BasePreferenceFragment {
|
||||
private static final String ZIP_MIME_TYPE = "application/zip";
|
||||
|
||||
private final SimpleDateFormat exportDateFormat =
|
||||
new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US);
|
||||
|
||||
private ContentSettingsManager manager;
|
||||
|
||||
private String importExportDataPathKey;
|
||||
private String youtubeRestrictedModeEnabledKey;
|
||||
|
||||
private Localization initialSelectedLocalization;
|
||||
private ContentCountry initialSelectedContentCountry;
|
||||
private String initialLanguage;
|
||||
private final ActivityResultLauncher<Intent> requestImportPathLauncher =
|
||||
registerForActivityResult(new StartActivityForResult(), this::requestImportPathResult);
|
||||
private final ActivityResultLauncher<Intent> requestExportPathLauncher =
|
||||
registerForActivityResult(new StartActivityForResult(), this::requestExportPathResult);
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) {
|
||||
final File homeDir = ContextCompat.getDataDir(requireContext());
|
||||
Objects.requireNonNull(homeDir);
|
||||
manager = new ContentSettingsManager(new NewPipeFileLocator(homeDir));
|
||||
manager.deleteSettingsFile();
|
||||
|
||||
importExportDataPathKey = getString(R.string.import_export_data_path);
|
||||
youtubeRestrictedModeEnabledKey = getString(R.string.youtube_restricted_mode_enabled);
|
||||
|
||||
addPreferencesFromResourceRegistry();
|
||||
|
||||
final Preference importDataPreference = requirePreference(R.string.import_data);
|
||||
importDataPreference.setOnPreferenceClickListener((Preference p) -> {
|
||||
NoFileManagerSafeGuard.launchSafe(
|
||||
requestImportPathLauncher,
|
||||
StoredFileHelper.getPicker(requireContext(),
|
||||
ZIP_MIME_TYPE, getImportExportDataUri()),
|
||||
TAG,
|
||||
getContext()
|
||||
);
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
final Preference exportDataPreference = requirePreference(R.string.export_data);
|
||||
exportDataPreference.setOnPreferenceClickListener((final Preference p) -> {
|
||||
NoFileManagerSafeGuard.launchSafe(
|
||||
requestExportPathLauncher,
|
||||
StoredFileHelper.getNewPicker(requireContext(),
|
||||
"NewPipeData-" + exportDateFormat.format(new Date()) + ".zip",
|
||||
ZIP_MIME_TYPE, getImportExportDataUri()),
|
||||
TAG,
|
||||
getContext()
|
||||
);
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
initialSelectedLocalization = org.schabi.newpipe.util.Localization
|
||||
.getPreferredLocalization(requireContext());
|
||||
initialSelectedContentCountry = org.schabi.newpipe.util.Localization
|
||||
|
@ -158,151 +88,4 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
|
|||
NewPipe.setupLocalization(selectedLocalization, selectedContentCountry);
|
||||
}
|
||||
}
|
||||
|
||||
private void requestExportPathResult(final ActivityResult result) {
|
||||
assureCorrectAppLanguage(getContext());
|
||||
if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
|
||||
// will be saved only on success
|
||||
final Uri lastExportDataUri = result.getData().getData();
|
||||
|
||||
final StoredFileHelper file =
|
||||
new StoredFileHelper(getContext(), result.getData().getData(), ZIP_MIME_TYPE);
|
||||
|
||||
exportDatabase(file, lastExportDataUri);
|
||||
}
|
||||
}
|
||||
|
||||
private void requestImportPathResult(final ActivityResult result) {
|
||||
assureCorrectAppLanguage(getContext());
|
||||
if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
|
||||
// will be saved only on success
|
||||
final Uri lastImportDataUri = result.getData().getData();
|
||||
|
||||
final StoredFileHelper file =
|
||||
new StoredFileHelper(getContext(), result.getData().getData(), ZIP_MIME_TYPE);
|
||||
|
||||
new AlertDialog.Builder(requireActivity())
|
||||
.setMessage(R.string.override_current_data)
|
||||
.setPositiveButton(R.string.ok, (d, id) ->
|
||||
importDatabase(file, lastImportDataUri))
|
||||
.setNegativeButton(R.string.cancel, (d, id) ->
|
||||
d.cancel())
|
||||
.show();
|
||||
}
|
||||
}
|
||||
|
||||
private void exportDatabase(final StoredFileHelper file, final Uri exportDataUri) {
|
||||
try {
|
||||
//checkpoint before export
|
||||
NewPipeDatabase.checkpoint();
|
||||
|
||||
final SharedPreferences preferences = PreferenceManager
|
||||
.getDefaultSharedPreferences(requireContext());
|
||||
manager.exportDatabase(preferences, file);
|
||||
|
||||
saveLastImportExportDataUri(exportDataUri); // save export path only on success
|
||||
Toast.makeText(getContext(), R.string.export_complete_toast, Toast.LENGTH_SHORT).show();
|
||||
} catch (final Exception e) {
|
||||
ErrorUtil.showUiErrorSnackbar(this, "Exporting database", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void importDatabase(final StoredFileHelper file, final Uri importDataUri) {
|
||||
// check if file is supported
|
||||
if (!ZipHelper.isValidZipFile(file)) {
|
||||
Toast.makeText(getContext(), R.string.no_valid_zip_file, Toast.LENGTH_SHORT)
|
||||
.show();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!manager.ensureDbDirectoryExists()) {
|
||||
throw new IOException("Could not create databases dir");
|
||||
}
|
||||
|
||||
if (!manager.extractDb(file)) {
|
||||
Toast.makeText(getContext(), R.string.could_not_import_all_files, Toast.LENGTH_LONG)
|
||||
.show();
|
||||
}
|
||||
|
||||
// if settings file exist, ask if it should be imported.
|
||||
if (manager.extractSettings(file)) {
|
||||
new AlertDialog.Builder(requireContext())
|
||||
.setTitle(R.string.import_settings)
|
||||
.setNegativeButton(R.string.cancel, (dialog, which) -> {
|
||||
dialog.dismiss();
|
||||
finishImport(importDataUri);
|
||||
})
|
||||
.setPositiveButton(R.string.ok, (dialog, which) -> {
|
||||
dialog.dismiss();
|
||||
final Context context = requireContext();
|
||||
final SharedPreferences prefs = PreferenceManager
|
||||
.getDefaultSharedPreferences(context);
|
||||
manager.loadSharedPreferences(prefs);
|
||||
cleanImport(context, prefs);
|
||||
finishImport(importDataUri);
|
||||
})
|
||||
.show();
|
||||
} else {
|
||||
finishImport(importDataUri);
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
ErrorUtil.showUiErrorSnackbar(this, "Importing database", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove settings that are not supposed to be imported on different devices
|
||||
* and reset them to default values.
|
||||
* @param context the context used for the import
|
||||
* @param prefs the preferences used while running the import
|
||||
*/
|
||||
private void cleanImport(@NonNull final Context context,
|
||||
@NonNull final SharedPreferences prefs) {
|
||||
// Check if media tunnelling needs to be disabled automatically,
|
||||
// if it was disabled automatically in the imported preferences.
|
||||
final String tunnelingKey = context.getString(R.string.disable_media_tunneling_key);
|
||||
final String automaticTunnelingKey =
|
||||
context.getString(R.string.disabled_media_tunneling_automatically_key);
|
||||
// R.string.disable_media_tunneling_key should always be true
|
||||
// if R.string.disabled_media_tunneling_automatically_key equals 1,
|
||||
// but we double check here just to be sure and to avoid regressions
|
||||
// caused by possible later modification of the media tunneling functionality.
|
||||
// R.string.disabled_media_tunneling_automatically_key == 0:
|
||||
// automatic value overridden by user in settings
|
||||
// R.string.disabled_media_tunneling_automatically_key == -1: not set
|
||||
final boolean wasMediaTunnelingDisabledAutomatically =
|
||||
prefs.getInt(automaticTunnelingKey, -1) == 1
|
||||
&& prefs.getBoolean(tunnelingKey, false);
|
||||
if (wasMediaTunnelingDisabledAutomatically) {
|
||||
prefs.edit()
|
||||
.putInt(automaticTunnelingKey, -1)
|
||||
.putBoolean(tunnelingKey, false)
|
||||
.apply();
|
||||
NewPipeSettings.setMediaTunneling(context);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save import path and restart system.
|
||||
*
|
||||
* @param importDataUri The import path to save
|
||||
*/
|
||||
private void finishImport(final Uri importDataUri) {
|
||||
// save import path only on success
|
||||
saveLastImportExportDataUri(importDataUri);
|
||||
// restart app to properly load db
|
||||
NavigationHelper.restartApp(requireActivity());
|
||||
}
|
||||
|
||||
private Uri getImportExportDataUri() {
|
||||
final String path = defaultPreferences.getString(importExportDataPathKey, null);
|
||||
return isBlank(path) ? null : Uri.parse(path);
|
||||
}
|
||||
|
||||
private void saveLastImportExportDataUri(final Uri importExportDataUri) {
|
||||
final SharedPreferences.Editor editor = defaultPreferences.edit()
|
||||
.putString(importExportDataPathKey, importExportDataUri.toString());
|
||||
editor.apply();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,120 +0,0 @@
|
|||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -23,7 +23,7 @@ public class MainSettingsFragment extends BasePreferenceFragment {
|
|||
setHasOptionsMenu(true); // Otherwise onCreateOptionsMenu is not called
|
||||
|
||||
// Check if the app is updatable
|
||||
if (!ReleaseVersionUtil.isReleaseApk()) {
|
||||
if (!ReleaseVersionUtil.INSTANCE.isReleaseApk()) {
|
||||
getPreferenceScreen().removePreference(
|
||||
findPreference(getString(R.string.update_pref_screen_key)));
|
||||
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
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") }
|
||||
}
|
|
@ -11,6 +11,7 @@ import androidx.annotation.NonNull;
|
|||
import androidx.annotation.StringRes;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import org.schabi.newpipe.App;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.util.DeviceUtils;
|
||||
|
||||
|
@ -44,14 +45,8 @@ public final class NewPipeSettings {
|
|||
private NewPipeSettings() { }
|
||||
|
||||
public static void initSettings(final Context context) {
|
||||
// check if the last used preference version is set
|
||||
// to determine whether this is the first app run
|
||||
final int lastUsedPrefVersion = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.getInt(context.getString(R.string.last_used_preferences_version), -1);
|
||||
final boolean isFirstRun = lastUsedPrefVersion == -1;
|
||||
|
||||
// first run migrations, then setDefaultValues, since the latter requires the correct types
|
||||
SettingMigrations.runMigrationsIfNeeded(context, isFirstRun);
|
||||
SettingMigrations.runMigrationsIfNeeded(context);
|
||||
|
||||
// readAgain is true so that if new settings are added their default value is set
|
||||
PreferenceManager.setDefaultValues(context, R.xml.main_settings, true);
|
||||
|
@ -63,11 +58,12 @@ public final class NewPipeSettings {
|
|||
PreferenceManager.setDefaultValues(context, R.xml.player_notification_settings, true);
|
||||
PreferenceManager.setDefaultValues(context, R.xml.update_settings, true);
|
||||
PreferenceManager.setDefaultValues(context, R.xml.debug_settings, true);
|
||||
PreferenceManager.setDefaultValues(context, R.xml.backup_restore_settings, true);
|
||||
|
||||
saveDefaultVideoDownloadDirectory(context);
|
||||
saveDefaultAudioDownloadDirectory(context);
|
||||
|
||||
disableMediaTunnelingIfNecessary(context, isFirstRun);
|
||||
disableMediaTunnelingIfNecessary(context);
|
||||
}
|
||||
|
||||
static void saveDefaultVideoDownloadDirectory(final Context context) {
|
||||
|
@ -145,8 +141,7 @@ public final class NewPipeSettings {
|
|||
R.string.show_remote_search_suggestions_key);
|
||||
}
|
||||
|
||||
private static void disableMediaTunnelingIfNecessary(@NonNull final Context context,
|
||||
final boolean isFirstRun) {
|
||||
private static void disableMediaTunnelingIfNecessary(@NonNull final Context context) {
|
||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
final String disabledTunnelingKey = context.getString(R.string.disable_media_tunneling_key);
|
||||
final String disabledTunnelingAutomaticallyKey =
|
||||
|
@ -161,7 +156,7 @@ public final class NewPipeSettings {
|
|||
prefs.getInt(disabledTunnelingAutomaticallyKey, -1) == 0
|
||||
&& !prefs.getBoolean(disabledTunnelingKey, false);
|
||||
|
||||
if (Boolean.TRUE.equals(isFirstRun)
|
||||
if (App.getApp().isFirstRun()
|
||||
|| (wasDeviceBlacklistUpdated && !wasMediaTunnelingEnabledByUser)) {
|
||||
setMediaTunneling(context);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
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;
|
||||
|
@ -31,7 +33,6 @@ 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 {
|
||||
|
@ -90,8 +91,7 @@ public class SelectPlaylistFragment extends DialogFragment {
|
|||
final LocalPlaylistManager localPlaylistManager = new LocalPlaylistManager(database);
|
||||
final RemotePlaylistManager remotePlaylistManager = new RemotePlaylistManager(database);
|
||||
|
||||
disposable = Flowable.combineLatest(localPlaylistManager.getPlaylists(),
|
||||
remotePlaylistManager.getPlaylists(), PlaylistLocalItem::merge)
|
||||
disposable = getMergedOrderedPlaylists(localPlaylistManager, remotePlaylistManager)
|
||||
.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.uid, entry.name);
|
||||
onSelectedListener.onLocalPlaylistSelected(entry.getUid(), entry.name);
|
||||
|
||||
} else if (selectedItem instanceof PlaylistRemoteEntity) {
|
||||
final PlaylistRemoteEntity entry = ((PlaylistRemoteEntity) selectedItem);
|
||||
|
|
|
@ -7,6 +7,7 @@ import android.util.Log;
|
|||
import androidx.annotation.NonNull;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import org.schabi.newpipe.App;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
import org.schabi.newpipe.error.ErrorUtil;
|
||||
|
@ -163,15 +164,14 @@ public final class SettingMigrations {
|
|||
private static final int VERSION = 6;
|
||||
|
||||
|
||||
public static void runMigrationsIfNeeded(@NonNull final Context context,
|
||||
final boolean isFirstRun) {
|
||||
public static void runMigrationsIfNeeded(@NonNull final Context context) {
|
||||
// setup migrations and check if there is something to do
|
||||
sp = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
final String lastPrefVersionKey = context.getString(R.string.last_used_preferences_version);
|
||||
final int lastPrefVersion = sp.getInt(lastPrefVersionKey, 0);
|
||||
|
||||
// no migration to run, already up to date
|
||||
if (isFirstRun) {
|
||||
if (App.getApp().isFirstRun()) {
|
||||
sp.edit().putInt(lastPrefVersionKey, VERSION).apply();
|
||||
return;
|
||||
} else if (lastPrefVersion == VERSION) {
|
||||
|
|
|
@ -266,7 +266,7 @@ public class SettingsActivity extends AppCompatActivity implements
|
|||
*/
|
||||
private void ensureSearchRepresentsApplicationState() {
|
||||
// Check if the update settings are available
|
||||
if (!ReleaseVersionUtil.isReleaseApk()) {
|
||||
if (!ReleaseVersionUtil.INSTANCE.isReleaseApk()) {
|
||||
SettingsResourceRegistry.getInstance()
|
||||
.getEntryByPreferencesResId(R.xml.update_settings)
|
||||
.setSearchable(false);
|
||||
|
|
|
@ -41,6 +41,7 @@ public final class SettingsResourceRegistry {
|
|||
add(UpdateSettingsFragment.class, R.xml.update_settings);
|
||||
add(VideoAudioSettingsFragment.class, R.xml.video_audio_settings);
|
||||
add(ExoPlayerSettingsFragment.class, R.xml.exoplayer_settings);
|
||||
add(BackupRestoreSettingsFragment.class, R.xml.backup_restore_settings);
|
||||
}
|
||||
|
||||
private SettingRegistryEntry add(
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
package org.schabi.newpipe.settings;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import org.schabi.newpipe.NewVersionWorker;
|
||||
import org.schabi.newpipe.R;
|
||||
|
@ -36,4 +39,38 @@ public class UpdateSettingsFragment extends BasePreferenceFragment {
|
|||
findPreference(getString(R.string.manual_update_key))
|
||||
.setOnPreferenceClickListener(manualUpdateClick);
|
||||
}
|
||||
|
||||
public static void askForConsentToUpdateChecks(final Context context) {
|
||||
new AlertDialog.Builder(context)
|
||||
.setTitle(context.getString(R.string.check_for_updates))
|
||||
.setMessage(context.getString(R.string.auto_update_check_description))
|
||||
.setPositiveButton(context.getString(R.string.yes), (d, w) -> {
|
||||
d.dismiss();
|
||||
setAutoUpdateCheckEnabled(context, true);
|
||||
})
|
||||
.setNegativeButton(R.string.no, (d, w) -> {
|
||||
d.dismiss();
|
||||
// set explicitly to false, since the default is true on previous versions
|
||||
setAutoUpdateCheckEnabled(context, false);
|
||||
})
|
||||
.show();
|
||||
}
|
||||
|
||||
private static void setAutoUpdateCheckEnabled(final Context context, final boolean enabled) {
|
||||
PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.edit()
|
||||
.putBoolean(context.getString(R.string.update_app_key), enabled)
|
||||
.putBoolean(context.getString(R.string.update_check_consent_key), true)
|
||||
.apply();
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the user was asked for consent to automatically check for app updates.
|
||||
* @param context
|
||||
* @return true if the user was asked for consent, false otherwise
|
||||
*/
|
||||
public static boolean wasUserAskedForConsent(final Context context) {
|
||||
return PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.getBoolean(context.getString(R.string.update_check_consent_key), false);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
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") }
|
||||
}
|
|
@ -0,0 +1,180 @@
|
|||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
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());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,11 +1,18 @@
|
|||
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.provider.DocumentsContract;
|
||||
import android.system.Os;
|
||||
import android.system.StructStatVfs;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
@ -15,6 +22,7 @@ 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;
|
||||
|
@ -26,10 +34,6 @@ import java.util.List;
|
|||
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;
|
||||
|
||||
public class StoredDirectoryHelper {
|
||||
private static final String TAG = StoredDirectoryHelper.class.getSimpleName();
|
||||
public static final int PERMISSION_FLAGS = Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
|
@ -38,6 +42,10 @@ 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;
|
||||
|
@ -168,6 +176,46 @@ public class StoredDirectoryHelper {
|
|||
return docTree == null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public long getFreeStorageSpace() {
|
||||
try {
|
||||
final StructStatVfs stat;
|
||||
|
||||
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());
|
||||
|
||||
} 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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Only using Java I/O. Creates the directory named by this abstract pathname, including any
|
||||
* necessary but nonexistent parent directories.
|
||||
|
|
|
@ -27,6 +27,7 @@ import android.util.Log;
|
|||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.text.HtmlCompat;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
@ -113,14 +114,14 @@ public final class ExtractorHelper {
|
|||
public static Single<StreamInfo> getStreamInfo(final int serviceId, final String url,
|
||||
final boolean forceLoad) {
|
||||
checkServiceId(serviceId);
|
||||
return checkCache(forceLoad, serviceId, url, InfoItem.InfoType.STREAM,
|
||||
return checkCache(forceLoad, serviceId, url, InfoCache.Type.STREAM,
|
||||
Single.fromCallable(() -> StreamInfo.getInfo(NewPipe.getService(serviceId), url)));
|
||||
}
|
||||
|
||||
public static Single<ChannelInfo> getChannelInfo(final int serviceId, final String url,
|
||||
final boolean forceLoad) {
|
||||
checkServiceId(serviceId);
|
||||
return checkCache(forceLoad, serviceId, url, InfoItem.InfoType.CHANNEL,
|
||||
return checkCache(forceLoad, serviceId, url, InfoCache.Type.CHANNEL,
|
||||
Single.fromCallable(() ->
|
||||
ChannelInfo.getInfo(NewPipe.getService(serviceId), url)));
|
||||
}
|
||||
|
@ -130,7 +131,7 @@ public final class ExtractorHelper {
|
|||
final boolean forceLoad) {
|
||||
checkServiceId(serviceId);
|
||||
return checkCache(forceLoad, serviceId,
|
||||
listLinkHandler.getUrl(), InfoItem.InfoType.CHANNEL,
|
||||
listLinkHandler.getUrl(), InfoCache.Type.CHANNEL_TAB,
|
||||
Single.fromCallable(() ->
|
||||
ChannelTabInfo.getInfo(NewPipe.getService(serviceId), listLinkHandler)));
|
||||
}
|
||||
|
@ -145,10 +146,11 @@ public final class ExtractorHelper {
|
|||
listLinkHandler, nextPage));
|
||||
}
|
||||
|
||||
public static Single<CommentsInfo> getCommentsInfo(final int serviceId, final String url,
|
||||
public static Single<CommentsInfo> getCommentsInfo(final int serviceId,
|
||||
final String url,
|
||||
final boolean forceLoad) {
|
||||
checkServiceId(serviceId);
|
||||
return checkCache(forceLoad, serviceId, url, InfoItem.InfoType.COMMENT,
|
||||
return checkCache(forceLoad, serviceId, url, InfoCache.Type.COMMENTS,
|
||||
Single.fromCallable(() ->
|
||||
CommentsInfo.getInfo(NewPipe.getService(serviceId), url)));
|
||||
}
|
||||
|
@ -175,7 +177,7 @@ public final class ExtractorHelper {
|
|||
final String url,
|
||||
final boolean forceLoad) {
|
||||
checkServiceId(serviceId);
|
||||
return checkCache(forceLoad, serviceId, url, InfoItem.InfoType.PLAYLIST,
|
||||
return checkCache(forceLoad, serviceId, url, InfoCache.Type.PLAYLIST,
|
||||
Single.fromCallable(() ->
|
||||
PlaylistInfo.getInfo(NewPipe.getService(serviceId), url)));
|
||||
}
|
||||
|
@ -188,9 +190,10 @@ public final class ExtractorHelper {
|
|||
PlaylistInfo.getMoreItems(NewPipe.getService(serviceId), url, nextPage));
|
||||
}
|
||||
|
||||
public static Single<KioskInfo> getKioskInfo(final int serviceId, final String url,
|
||||
public static Single<KioskInfo> getKioskInfo(final int serviceId,
|
||||
final String url,
|
||||
final boolean forceLoad) {
|
||||
return checkCache(forceLoad, serviceId, url, InfoItem.InfoType.PLAYLIST,
|
||||
return checkCache(forceLoad, serviceId, url, InfoCache.Type.KIOSK,
|
||||
Single.fromCallable(() -> KioskInfo.getInfo(NewPipe.getService(serviceId), url)));
|
||||
}
|
||||
|
||||
|
@ -202,7 +205,7 @@ public final class ExtractorHelper {
|
|||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
// Cache
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
/**
|
||||
|
@ -214,24 +217,25 @@ public final class ExtractorHelper {
|
|||
* @param forceLoad whether to force loading from the network instead of from the cache
|
||||
* @param serviceId the service to load from
|
||||
* @param url the URL to load
|
||||
* @param infoType the {@link InfoItem.InfoType} of the item
|
||||
* @param cacheType the {@link InfoCache.Type} of the item
|
||||
* @param loadFromNetwork the {@link Single} to load the item from the network
|
||||
* @return a {@link Single} that loads the item
|
||||
*/
|
||||
private static <I extends Info> Single<I> checkCache(final boolean forceLoad,
|
||||
final int serviceId, final String url,
|
||||
final InfoItem.InfoType infoType,
|
||||
final Single<I> loadFromNetwork) {
|
||||
final int serviceId,
|
||||
@NonNull final String url,
|
||||
@NonNull final InfoCache.Type cacheType,
|
||||
@NonNull final Single<I> loadFromNetwork) {
|
||||
checkServiceId(serviceId);
|
||||
final Single<I> actualLoadFromNetwork = loadFromNetwork
|
||||
.doOnSuccess(info -> CACHE.putInfo(serviceId, url, info, infoType));
|
||||
.doOnSuccess(info -> CACHE.putInfo(serviceId, url, info, cacheType));
|
||||
|
||||
final Single<I> load;
|
||||
if (forceLoad) {
|
||||
CACHE.removeInfo(serviceId, url, infoType);
|
||||
CACHE.removeInfo(serviceId, url, cacheType);
|
||||
load = actualLoadFromNetwork;
|
||||
} else {
|
||||
load = Maybe.concat(ExtractorHelper.loadFromCache(serviceId, url, infoType),
|
||||
load = Maybe.concat(ExtractorHelper.loadFromCache(serviceId, url, cacheType),
|
||||
actualLoadFromNetwork.toMaybe())
|
||||
.firstElement() // Take the first valid
|
||||
.toSingle();
|
||||
|
@ -246,15 +250,17 @@ public final class ExtractorHelper {
|
|||
* @param <I> the item type's class that extends {@link Info}
|
||||
* @param serviceId the service to load from
|
||||
* @param url the URL to load
|
||||
* @param infoType the {@link InfoItem.InfoType} of the item
|
||||
* @param cacheType the {@link InfoCache.Type} of the item
|
||||
* @return a {@link Single} that loads the item
|
||||
*/
|
||||
private static <I extends Info> Maybe<I> loadFromCache(final int serviceId, final String url,
|
||||
final InfoItem.InfoType infoType) {
|
||||
private static <I extends Info> Maybe<I> loadFromCache(
|
||||
final int serviceId,
|
||||
@NonNull final String url,
|
||||
@NonNull final InfoCache.Type cacheType) {
|
||||
checkServiceId(serviceId);
|
||||
return Maybe.defer(() -> {
|
||||
//noinspection unchecked
|
||||
final I info = (I) CACHE.getFromKey(serviceId, url, infoType);
|
||||
final I info = (I) CACHE.getFromKey(serviceId, url, cacheType);
|
||||
if (MainActivity.DEBUG) {
|
||||
Log.d(TAG, "loadFromCache() called, info > " + info);
|
||||
}
|
||||
|
@ -268,11 +274,17 @@ public final class ExtractorHelper {
|
|||
});
|
||||
}
|
||||
|
||||
public static boolean isCached(final int serviceId, final String url,
|
||||
final InfoItem.InfoType infoType) {
|
||||
return null != loadFromCache(serviceId, url, infoType).blockingGet();
|
||||
public static boolean isCached(final int serviceId,
|
||||
@NonNull final String url,
|
||||
@NonNull final InfoCache.Type cacheType) {
|
||||
return null != loadFromCache(serviceId, url, cacheType).blockingGet();
|
||||
}
|
||||
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
/**
|
||||
* Formats the text contained in the meta info list as HTML and puts it into the text view,
|
||||
* while also making the separator visible. If the list is null or empty, or the user chose not
|
||||
|
|
|
@ -27,7 +27,6 @@ import androidx.collection.LruCache;
|
|||
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
import org.schabi.newpipe.extractor.Info;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
|
@ -48,14 +47,27 @@ public final class InfoCache {
|
|||
// no instance
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies the type of {@link Info} to put into the cache.
|
||||
*/
|
||||
public enum Type {
|
||||
STREAM,
|
||||
CHANNEL,
|
||||
CHANNEL_TAB,
|
||||
COMMENTS,
|
||||
PLAYLIST,
|
||||
KIOSK,
|
||||
}
|
||||
|
||||
public static InfoCache getInstance() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static String keyOf(final int serviceId, @NonNull final String url,
|
||||
@NonNull final InfoItem.InfoType infoType) {
|
||||
return serviceId + url + infoType.toString();
|
||||
private static String keyOf(final int serviceId,
|
||||
@NonNull final String url,
|
||||
@NonNull final Type cacheType) {
|
||||
return serviceId + ":" + cacheType.ordinal() + ":" + url;
|
||||
}
|
||||
|
||||
private static void removeStaleCache() {
|
||||
|
@ -83,19 +95,22 @@ public final class InfoCache {
|
|||
}
|
||||
|
||||
@Nullable
|
||||
public Info getFromKey(final int serviceId, @NonNull final String url,
|
||||
@NonNull final InfoItem.InfoType infoType) {
|
||||
public Info getFromKey(final int serviceId,
|
||||
@NonNull final String url,
|
||||
@NonNull final Type cacheType) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "getFromKey() called with: "
|
||||
+ "serviceId = [" + serviceId + "], url = [" + url + "]");
|
||||
}
|
||||
synchronized (LRU_CACHE) {
|
||||
return getInfo(keyOf(serviceId, url, infoType));
|
||||
return getInfo(keyOf(serviceId, url, cacheType));
|
||||
}
|
||||
}
|
||||
|
||||
public void putInfo(final int serviceId, @NonNull final String url, @NonNull final Info info,
|
||||
@NonNull final InfoItem.InfoType infoType) {
|
||||
public void putInfo(final int serviceId,
|
||||
@NonNull final String url,
|
||||
@NonNull final Info info,
|
||||
@NonNull final Type cacheType) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "putInfo() called with: info = [" + info + "]");
|
||||
}
|
||||
|
@ -103,18 +118,19 @@ public final class InfoCache {
|
|||
final long expirationMillis = ServiceHelper.getCacheExpirationMillis(info.getServiceId());
|
||||
synchronized (LRU_CACHE) {
|
||||
final CacheData data = new CacheData(info, expirationMillis);
|
||||
LRU_CACHE.put(keyOf(serviceId, url, infoType), data);
|
||||
LRU_CACHE.put(keyOf(serviceId, url, cacheType), data);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeInfo(final int serviceId, @NonNull final String url,
|
||||
@NonNull final InfoItem.InfoType infoType) {
|
||||
public void removeInfo(final int serviceId,
|
||||
@NonNull final String url,
|
||||
@NonNull final Type cacheType) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "removeInfo() called with: "
|
||||
+ "serviceId = [" + serviceId + "], url = [" + url + "]");
|
||||
}
|
||||
synchronized (LRU_CACHE) {
|
||||
LRU_CACHE.remove(keyOf(serviceId, url, infoType));
|
||||
LRU_CACHE.remove(keyOf(serviceId, url, cacheType));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -643,6 +643,7 @@ public final class ListHelper {
|
|||
context.getString(R.string.best_resolution_key), defaultFormat, videoStreams);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static MediaFormat getDefaultFormat(@NonNull final Context context,
|
||||
@StringRes final int defaultFormatKey,
|
||||
@StringRes final int defaultFormatValueKey) {
|
||||
|
@ -651,18 +652,14 @@ public final class ListHelper {
|
|||
|
||||
final String defaultFormat = context.getString(defaultFormatValueKey);
|
||||
final String defaultFormatString = preferences.getString(
|
||||
context.getString(defaultFormatKey), defaultFormat);
|
||||
context.getString(defaultFormatKey),
|
||||
defaultFormat
|
||||
);
|
||||
|
||||
MediaFormat defaultMediaFormat = getMediaFormatFromKey(context, defaultFormatString);
|
||||
if (defaultMediaFormat == null) {
|
||||
preferences.edit().putString(context.getString(defaultFormatKey), defaultFormat)
|
||||
.apply();
|
||||
defaultMediaFormat = getMediaFormatFromKey(context, defaultFormat);
|
||||
}
|
||||
|
||||
return defaultMediaFormat;
|
||||
return getMediaFormatFromKey(context, defaultFormatString);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static MediaFormat getMediaFormatFromKey(@NonNull final Context context,
|
||||
@NonNull final String formatKey) {
|
||||
MediaFormat format = null;
|
||||
|
@ -877,6 +874,7 @@ public final class ListHelper {
|
|||
|
||||
return Comparator.comparing(AudioStream::getAudioLocale, Comparator.nullsLast(
|
||||
Comparator.comparing(locale -> locale.getDisplayName(appLoc))))
|
||||
.thenComparing(AudioStream::getAudioTrackType);
|
||||
.thenComparing(AudioStream::getAudioTrackType, Comparator.nullsLast(
|
||||
Comparator.naturalOrder()));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -238,7 +238,27 @@ public final class Localization {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a readable text for a duration in the format {@code days:hours:minutes:seconds}.
|
||||
* Prepended zeros are removed.
|
||||
* @param duration the duration in seconds
|
||||
* @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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a readable text for a duration in the format {@code days:hours:minutes:seconds+}.
|
||||
* Prepended zeros are removed. If the given duration is incomplete, a plus is appended to the
|
||||
* 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) {
|
||||
final String output;
|
||||
|
||||
final long days = duration / (24 * 60 * 60L); /* greater than a day */
|
||||
|
@ -256,7 +276,9 @@ public final class Localization {
|
|||
} else {
|
||||
output = String.format(Locale.US, "%d:%02d", minutes, seconds);
|
||||
}
|
||||
return output;
|
||||
final String durationPrefix = showDurationPrefix ? "⏱ " : "";
|
||||
final String durationPostfix = isDurationComplete ? "" : "+";
|
||||
return durationPrefix + output + durationPostfix;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,97 +1,39 @@
|
|||
package org.schabi.newpipe.util
|
||||
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.pm.Signature
|
||||
import androidx.core.content.pm.PackageInfoCompat
|
||||
import org.schabi.newpipe.App
|
||||
import org.schabi.newpipe.error.ErrorInfo
|
||||
import org.schabi.newpipe.error.ErrorUtil.Companion.createNotification
|
||||
import org.schabi.newpipe.error.UserAction
|
||||
import java.security.MessageDigest
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import java.security.cert.CertificateEncodingException
|
||||
import java.security.cert.CertificateException
|
||||
import java.security.cert.CertificateFactory
|
||||
import java.security.cert.X509Certificate
|
||||
import java.time.Instant
|
||||
import java.time.ZonedDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
object ReleaseVersionUtil {
|
||||
// Public key of the certificate that is used in NewPipe release versions
|
||||
private const val RELEASE_CERT_PUBLIC_KEY_SHA1 =
|
||||
"B0:2E:90:7C:1C:D6:FC:57:C3:35:F0:88:D0:8F:50:5F:94:E4:D2:15"
|
||||
private const val RELEASE_CERT_PUBLIC_KEY_SHA256 =
|
||||
"cb84069bd68116bafae5ee4ee5b08a567aa6d898404e7cb12f9e756df5cf5cab"
|
||||
|
||||
@JvmStatic
|
||||
fun isReleaseApk(): Boolean {
|
||||
return certificateSHA1Fingerprint == RELEASE_CERT_PUBLIC_KEY_SHA1
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to get the APK's SHA1 key. See https://stackoverflow.com/questions/9293019/#22506133.
|
||||
*
|
||||
* @return String with the APK's SHA1 fingerprint in hexadecimal
|
||||
*/
|
||||
private val certificateSHA1Fingerprint: String
|
||||
get() {
|
||||
val app = App.getApp()
|
||||
val signatures: List<Signature> = try {
|
||||
PackageInfoCompat.getSignatures(app.packageManager, app.packageName)
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
showRequestError(app, e, "Could not find package info")
|
||||
return ""
|
||||
}
|
||||
if (signatures.isEmpty()) {
|
||||
return ""
|
||||
}
|
||||
val x509cert = try {
|
||||
val cf = CertificateFactory.getInstance("X509")
|
||||
cf.generateCertificate(signatures[0].toByteArray().inputStream()) as X509Certificate
|
||||
} catch (e: CertificateException) {
|
||||
showRequestError(app, e, "Certificate error")
|
||||
return ""
|
||||
}
|
||||
|
||||
return try {
|
||||
val md = MessageDigest.getInstance("SHA1")
|
||||
val publicKey = md.digest(x509cert.encoded)
|
||||
byte2HexFormatted(publicKey)
|
||||
} catch (e: NoSuchAlgorithmException) {
|
||||
showRequestError(app, e, "Could not retrieve SHA1 key")
|
||||
""
|
||||
} catch (e: CertificateEncodingException) {
|
||||
showRequestError(app, e, "Could not retrieve SHA1 key")
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
private fun byte2HexFormatted(arr: ByteArray): String {
|
||||
val str = StringBuilder(arr.size * 2)
|
||||
for (i in arr.indices) {
|
||||
var h = Integer.toHexString(arr[i].toInt())
|
||||
val l = h.length
|
||||
if (l == 1) {
|
||||
h = "0$h"
|
||||
}
|
||||
if (l > 2) {
|
||||
h = h.substring(l - 2, l)
|
||||
}
|
||||
str.append(h.uppercase())
|
||||
if (i < arr.size - 1) {
|
||||
str.append(':')
|
||||
}
|
||||
}
|
||||
return str.toString()
|
||||
}
|
||||
|
||||
private fun showRequestError(app: App, e: Exception, request: String) {
|
||||
createNotification(
|
||||
app, ErrorInfo(e, UserAction.CHECK_FOR_NEW_APP_VERSION, request)
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
val isReleaseApk by lazy {
|
||||
@Suppress("NewApi")
|
||||
val certificates = mapOf(
|
||||
RELEASE_CERT_PUBLIC_KEY_SHA256.hexToByteArray() to PackageManager.CERT_INPUT_SHA256
|
||||
)
|
||||
val app = App.getApp()
|
||||
try {
|
||||
PackageInfoCompat.hasSignatures(app.packageManager, app.packageName, certificates, false)
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
createNotification(
|
||||
app, ErrorInfo(e, UserAction.CHECK_FOR_NEW_APP_VERSION, "Could not find package info")
|
||||
)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun isLastUpdateCheckExpired(expiry: Long): Boolean {
|
||||
return Instant.ofEpochSecond(expiry).isBefore(Instant.now())
|
||||
return Instant.ofEpochSecond(expiry) < Instant.now()
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -11,7 +11,6 @@ import org.schabi.newpipe.extractor.stream.Stream;
|
|||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||
import org.schabi.newpipe.util.StreamItemAdapter.StreamInfoWrapper;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
public class SecondaryStreamHelper<T extends Stream> {
|
||||
|
@ -43,42 +42,27 @@ public class SecondaryStreamHelper<T extends Stream> {
|
|||
@NonNull final List<AudioStream> audioStreams,
|
||||
@NonNull final VideoStream videoStream) {
|
||||
final MediaFormat mediaFormat = videoStream.getFormat();
|
||||
if (mediaFormat == null) {
|
||||
|
||||
if (mediaFormat == MediaFormat.WEBM) {
|
||||
return audioStreams
|
||||
.stream()
|
||||
.filter(audioStream -> audioStream.getFormat() == MediaFormat.WEBMA
|
||||
|| audioStream.getFormat() == MediaFormat.WEBMA_OPUS)
|
||||
.max(ListHelper.getAudioFormatComparator(MediaFormat.WEBMA,
|
||||
ListHelper.isLimitingDataUsage(context)))
|
||||
.orElse(null);
|
||||
|
||||
} else if (mediaFormat == MediaFormat.MPEG_4) {
|
||||
return audioStreams
|
||||
.stream()
|
||||
.filter(audioStream -> audioStream.getFormat() == MediaFormat.M4A)
|
||||
.max(ListHelper.getAudioFormatComparator(MediaFormat.M4A,
|
||||
ListHelper.isLimitingDataUsage(context)))
|
||||
.orElse(null);
|
||||
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (mediaFormat) {
|
||||
case WEBM:
|
||||
case MPEG_4: // Is MPEG-4 DASH?
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
final boolean m4v = mediaFormat == MediaFormat.MPEG_4;
|
||||
final boolean isLimitingDataUsage = ListHelper.isLimitingDataUsage(context);
|
||||
|
||||
Comparator<AudioStream> comparator = ListHelper.getAudioFormatComparator(
|
||||
m4v ? MediaFormat.M4A : MediaFormat.WEBMA, isLimitingDataUsage);
|
||||
int preferredAudioStreamIndex = ListHelper.getAudioIndexByHighestRank(
|
||||
audioStreams, comparator);
|
||||
|
||||
if (preferredAudioStreamIndex == -1) {
|
||||
if (m4v) {
|
||||
return null;
|
||||
}
|
||||
|
||||
comparator = ListHelper.getAudioFormatComparator(
|
||||
MediaFormat.WEBMA_OPUS, isLimitingDataUsage);
|
||||
preferredAudioStreamIndex = ListHelper.getAudioIndexByHighestRank(
|
||||
audioStreams, comparator);
|
||||
|
||||
if (preferredAudioStreamIndex == -1) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return audioStreams.get(preferredAudioStreamIndex);
|
||||
}
|
||||
|
||||
public T getStream() {
|
||||
|
|
|
@ -1,18 +1,21 @@
|
|||
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>
|
||||
|
@ -34,73 +37,154 @@ import org.schabi.newpipe.streams.io.StoredFileHelper;
|
|||
*/
|
||||
|
||||
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 override the original file.
|
||||
* 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 file The path of the file that should be added to zip.
|
||||
* @param name The path of the file inside the zip.
|
||||
* @throws Exception
|
||||
* @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
|
||||
*/
|
||||
public static void addFileToZip(final ZipOutputStream outZip, final String file,
|
||||
final String name) 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 {
|
||||
final byte[] data = new byte[BUFFER_SIZE];
|
||||
try (FileInputStream fi = new FileInputStream(file);
|
||||
BufferedInputStream inputStream = new BufferedInputStream(fi, BUFFER_SIZE)) {
|
||||
final ZipEntry entry = new ZipEntry(name);
|
||||
try (BufferedInputStream bufferedInputStream =
|
||||
new BufferedInputStream(inputStream, BUFFER_SIZE)) {
|
||||
final ZipEntry entry = new ZipEntry(nameInZip);
|
||||
outZip.putNextEntry(entry);
|
||||
int count;
|
||||
while ((count = inputStream.read(data, 0, BUFFER_SIZE)) != -1) {
|
||||
while ((count = bufferedInputStream.read(data, 0, BUFFER_SIZE)) != -1) {
|
||||
outZip.write(data, 0, count);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This will extract data from ZipInputStream.
|
||||
* Caution this will override the original file.
|
||||
* This will extract data from ZipInputStream. Caution this will overwrite the original file.
|
||||
*
|
||||
* @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.
|
||||
* @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
|
||||
* @throws Exception
|
||||
*/
|
||||
public static boolean extractFileFromZip(final StoredFileHelper zipFile, final String file,
|
||||
final String name) throws Exception {
|
||||
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.
|
||||
*
|
||||
* @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
|
||||
* @return will return true if the file was found within the zip file
|
||||
*/
|
||||
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 {
|
||||
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(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();
|
||||
if (ze.getName().equals(fileInZip)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return found;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
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);
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
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());
|
||||
}
|
||||
}
|
|
@ -92,7 +92,7 @@ public final class TextLinkifier {
|
|||
* {@link HtmlCompat#fromHtml(String, int)}.
|
||||
* </p>
|
||||
*
|
||||
* @param textView the {@link TextView} to set the the HTML string block linked
|
||||
* @param textView the {@link TextView} to set the HTML string block linked
|
||||
* @param htmlBlock the HTML string block to be linked
|
||||
* @param htmlCompatFlag the int flag to be set when {@link HtmlCompat#fromHtml(String,
|
||||
* int)} will be called
|
||||
|
|
|
@ -411,7 +411,7 @@ public class DownloadManagerService extends Service {
|
|||
mission.threadCount = threads;
|
||||
mission.source = source;
|
||||
mission.nearLength = nearLength;
|
||||
mission.recoveryInfo = recovery.toArray(MissionRecoveryInfo[]::new);
|
||||
mission.recoveryInfo = recovery.toArray(new MissionRecoveryInfo[0]);
|
||||
|
||||
if (ps != null)
|
||||
ps.setTemporalDir(DownloadManager.pickAvailableTemporalDir(this));
|
||||
|
|
|
@ -490,7 +490,7 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
|
|||
showError(mission, UserAction.DOWNLOAD_POSTPROCESSING, R.string.error_postprocessing_failed);
|
||||
return;
|
||||
case ERROR_INSUFFICIENT_STORAGE:
|
||||
msg = R.string.error_insufficient_storage;
|
||||
msg = R.string.error_insufficient_storage_left;
|
||||
break;
|
||||
case ERROR_UNKNOWN_EXCEPTION:
|
||||
if (mission.errObject != null) {
|
||||
|
|
|
@ -2,6 +2,8 @@ package us.shandian.giga.util;
|
|||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.os.Environment;
|
||||
import android.os.StatFs;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
|
@ -26,10 +28,8 @@ import java.io.ObjectOutputStream;
|
|||
import java.io.Serializable;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.util.Locale;
|
||||
import java.util.Random;
|
||||
|
||||
import okio.ByteString;
|
||||
import us.shandian.giga.get.DownloadMission;
|
||||
|
||||
public class Utility {
|
||||
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
../layout/list_stream_item.xml
|
|
@ -0,0 +1,84 @@
|
|||
<?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>
|
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
|
@ -78,7 +78,7 @@
|
|||
<string name="short_billion">بليون</string>
|
||||
<string name="feed_load_error_account_info">تعذر تحميل موجز \'%s\'.</string>
|
||||
<string name="question_mark">؟</string>
|
||||
<string name="manual_update_title">التحقق من وجود تحديثات</string>
|
||||
<string name="check_for_updates">التحقق من وجود تحديثات</string>
|
||||
<string name="peertube_instance_url_title">مثيلات خوادم پيرتيوب</string>
|
||||
<string name="more_than_100_videos">+100 فيديو</string>
|
||||
<string name="short_thousand">ألف</string>
|
||||
|
@ -271,7 +271,7 @@
|
|||
<string name="import_data_summary">يلغي السجل الحالي والاشتراكات وقوائم التشغيل والإعدادات (اختياريًا)</string>
|
||||
<string name="app_ui_crash">تعطل التطبيق / واجهة المستخدم</string>
|
||||
<string name="rename">إعادة التسمية</string>
|
||||
<string name="error_insufficient_storage">لم يتبقى مساحة في الجهاز</string>
|
||||
<string name="error_insufficient_storage_left">لم يتبقى مساحة في الجهاز</string>
|
||||
<string name="could_not_setup_download_menu">تعذر إعداد قائمة التنزيل</string>
|
||||
<string name="download_path_dialog_title">اختر مجلد التنزيل لملفات الفيديو</string>
|
||||
<string name="notifications_disabled">تم تعطيل الإشعارات</string>
|
||||
|
|
|
@ -407,7 +407,7 @@
|
|||
<string name="overwrite_failed">لا يمكن الكتابة فوق الملف</string>
|
||||
<string name="download_already_pending">هناك تنزيل معلق بهذا الاسم</string>
|
||||
<string name="error_postprocessing_stopped">تم إغلاق NewPipe أثناء العمل على الملف</string>
|
||||
<string name="error_insufficient_storage">لم يتبقى مساحة في الجهاز</string>
|
||||
<string name="error_insufficient_storage_left">لم يتبقى مساحة في الجهاز</string>
|
||||
<string name="error_progress_lost">تم فقد التقدم بسبب حذف الملف</string>
|
||||
<string name="error_timeout">انتهى وقت الاتصال</string>
|
||||
<string name="confirm_prompt">هل تريد محو سجل التنزيل، أم تريد حذف جميع الملفات التي تم تنزيلها؟</string>
|
||||
|
@ -584,7 +584,7 @@
|
|||
<string name="notification_action_shuffle">خلط</string>
|
||||
<string name="notification_action_repeat">تكرار</string>
|
||||
<string name="notification_actions_at_most_three">يمكنك تحديد ثلاثة إجراءات كحد أقصى لإظهارها في الإشعار المضغوط!</string>
|
||||
<string name="notification_actions_summary">قم بتحرير كل إشعار أدناه من خلال النقر عليه. حدد ما يصل إلى ثلاثة منها لتظهر في الإشعار المضغوط باستخدام مربعات الاختيار الموجودة على اليمين</string>
|
||||
<string name="notification_actions_summary">قم بتحرير كل إجراء إعلام أدناه من خلال النقر عليه. حدد ما يصل إلى ثلاثة منها ليتم عرضها في الإشعار المضغوط باستخدام مربعات الاختيار الموجودة على اليمين.</string>
|
||||
<string name="notification_action_4_title">زر الإجراء الخامس</string>
|
||||
<string name="notification_action_3_title">زر الإجراء الرابع</string>
|
||||
<string name="notification_action_2_title">زر الإجراء الثالث</string>
|
||||
|
@ -700,7 +700,7 @@
|
|||
<string name="enqueue_next_stream">وضع التالي على قائمة الانتظار</string>
|
||||
<string name="enqueued_next">تم وضع التالي على قائمة الانتظار</string>
|
||||
<string name="processing_may_take_a_moment">جاري المعالجة ... قد يستغرق لحظة</string>
|
||||
<string name="manual_update_title">التحقق من وجود تحديثات</string>
|
||||
<string name="check_for_updates">التحقق من وجود تحديثات</string>
|
||||
<string name="manual_update_description">التحقق يدويا من وجود إصدارات جديدة</string>
|
||||
<string name="checking_updates_toast">جاري التحقق من وجود تحديثات…</string>
|
||||
<string name="feed_new_items">عناصر تغذية جديدة</string>
|
||||
|
@ -858,4 +858,26 @@
|
|||
<string name="share_playlist">مشاركة قائمة التشغيل</string>
|
||||
<string name="share_playlist_with_titles_message">شارك تفاصيل قائمة التشغيل مثل اسم قائمة التشغيل وعناوين الفيديو أو كقائمة بسيطة من عناوين URL للفيديو</string>
|
||||
<string name="video_details_list_item">- %1$s: %2$s</string>
|
||||
<plurals name="replies">
|
||||
<item quantity="zero">رد %s</item>
|
||||
<item quantity="one">رد %s</item>
|
||||
<item quantity="two">ردان%s</item>
|
||||
<item quantity="few">ردود%s</item>
|
||||
<item quantity="many">ردود %s</item>
|
||||
<item quantity="other">ردود %s</item>
|
||||
</plurals>
|
||||
<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>
|
|
@ -448,7 +448,7 @@
|
|||
<item quantity="one">%s video</item>
|
||||
<item quantity="other">%s video</item>
|
||||
</plurals>
|
||||
<string name="manual_update_title">Yeniləmələri yoxla</string>
|
||||
<string name="check_for_updates">Yeniləmələri yoxla</string>
|
||||
<string name="seekbar_preview_thumbnail_title">Axtarış çubuğunun miniatür önizləməsi</string>
|
||||
<string name="permission_denied">Əməliyyat sistem tərəfindən ləğv edildi</string>
|
||||
<string name="auto">Avto</string>
|
||||
|
@ -548,7 +548,7 @@
|
|||
<string name="remove_watched">İzləniləni sil</string>
|
||||
<string name="downloads_storage_use_saf_title">Sistem qovluğu seçicisini (SAF) istifadə et</string>
|
||||
<string name="error_timeout">Bağlantı fasiləsi</string>
|
||||
<string name="error_insufficient_storage">Cihazda yer qalmayıb</string>
|
||||
<string name="error_insufficient_storage_left">Cihazda yer qalmayıb</string>
|
||||
<string name="error_postprocessing_stopped">Fayl üzərində işləyərkən NewPipe bağlandı</string>
|
||||
<string name="error_postprocessing_failed">Emaldan sonra uğursuz oldu</string>
|
||||
<string name="error_connect_host">Serverə qoşulmaq mümkün deyil</string>
|
||||
|
@ -694,7 +694,7 @@
|
|||
<string name="youtube_music_premium_content">Bu video yalnız YouTube Music Premium üzvləri üçün əlçatandır, ona görə də NewPipe tərəfindən yayımlamaq və ya endirmək mümkün deyil.</string>
|
||||
<string name="description_select_note">İndi açıqlamadakı mətni seçə bilərsiniz. Nəzərə alın ki, seçim rejimində səhifə titrəyə və linklər kliklənməyə bilər.</string>
|
||||
<string name="notification_scale_to_square_image_summary">Bildirişdə göstərilən video miniatürünü 16:9-dan 1:1 görünüş nisbətinə qədər kəs</string>
|
||||
<string name="notification_actions_summary">Aşağıdakı hər bir bildiriş fəaliyyətini üzərinə toxunaraq redaktə et. Sağdakı təsdiq qutularından istifadə edərək yığcam bildirişdə göstərmək üçün onların üçünü seç</string>
|
||||
<string name="notification_actions_summary">Aşağıdakı hər bir bildiriş fəaliyyətini üzərinə toxunaraq düzəliş edin. Sağdakı təsdiq qutularından istifadə edərək yığcam bildirişdə göstərmək üçün onların üçünü seçin.</string>
|
||||
<string name="invalid_source">Belə fayl/məzmun mənbəyi yoxdur</string>
|
||||
<string name="selected_stream_external_player_not_supported">Seçilən yayım xarici oynadıcılar tərəfindən dəstəklənmir</string>
|
||||
<string name="streams_not_yet_supported_removed">Yükləyici tərəfindən hələ dəstəklənməyən yayımlar göstərilmir</string>
|
||||
|
@ -769,4 +769,5 @@
|
|||
<string name="feed_fetch_channel_tabs_summary">Axın yenilənərkən əldə edilən səhifələr.Kanal sürətli rejim istifadə edərək yenilənirsə, bu seçimin heç bir təsiri yoxdur.</string>
|
||||
<string name="metadata_uploader_avatars">Yükləyici avatarları</string>
|
||||
<string name="metadata_thumbnails">Miniatürlər</string>
|
||||
<string name="notification_actions_summary_android13">Aşağıdakı hər bildirişə vuraraq ona düzəliş edin. İlk üç əməl (oynatma/fasilə, əvvəlki və sonrakı) sistem tərəfindən təyin olunub və dəyişdirilə bilməz.</string>
|
||||
</resources>
|
|
@ -171,7 +171,7 @@
|
|||
<string name="overwrite_finished_warning">Yá esiste un ficheru baxáu con esti nome</string>
|
||||
<string name="overwrite_failed">nun pue sobrecribise\'l ficheru</string>
|
||||
<string name="download_already_pending">Hai una descarga pendiente con esti nome</string>
|
||||
<string name="error_insufficient_storage">Nun queda espaciu nel preséu</string>
|
||||
<string name="error_insufficient_storage_left">Nun queda espaciu nel preséu</string>
|
||||
<string name="error_timeout">Escosó\'l tiempu d\'espera de la conexón</string>
|
||||
<string name="subscriptions_import_unsuccessful">Nun pudieron importase les soscripciones</string>
|
||||
<string name="caption_setting_title">Sotítulos</string>
|
||||
|
|
|
@ -58,7 +58,7 @@
|
|||
<string name="play_with_kodi_title">Kodi bilan ijro etish</string>
|
||||
<string name="show_higher_resolutions_summary">Faqat ba\'zi qurilmalar 2K / 4K videolarni ijro etishi mumkin</string>
|
||||
<string name="show_higher_resolutions_title">Yuqori o\'lchamlarni ko\'rsatish</string>
|
||||
<string name="default_popup_resolution_title">"Standart pop-up o\'lchamlari"</string>
|
||||
<string name="default_popup_resolution_title">Standart pop-up o\'lchamlari</string>
|
||||
<string name="default_resolution_title">Standart o\'lchamlari</string>
|
||||
<string name="download_path_audio_dialog_title">Audio fayllar uchun yuklab olish papkasini tanlash</string>
|
||||
<string name="download_path_summary">Yuklab olingan videofayllar shu yerda saqlanadi</string>
|
||||
|
@ -416,7 +416,7 @@
|
|||
<string name="error_download_resource_gone">Ushbu yuklab olishni tiklab bo\'lmaydi</string>
|
||||
<string name="error_timeout">Ulanish vaqti tugadi</string>
|
||||
<string name="error_progress_lost">Siljish yo\'qoldi, chunki fayl o\'chirildi</string>
|
||||
<string name="error_insufficient_storage">Qurilmada bo\'sh joy qolmadi</string>
|
||||
<string name="error_insufficient_storage_left">Qurilmada bo\'sh joy qolmadi</string>
|
||||
<string name="error_postprocessing_stopped">NewPipe fayl ustida ishlash paytida yopilgan</string>
|
||||
<string name="error_postprocessing_failed">Keyingi ishlov berilmadi</string>
|
||||
<string name="error_http_not_found">Topilmadi</string>
|
||||
|
|
|
@ -6,8 +6,8 @@
|
|||
<string name="no_player_found_toast">Патокавы плэер не знойдзены (вы можаце ўсталяваць VLC каб прайграць).</string>
|
||||
<string name="install">Усталяваць</string>
|
||||
<string name="cancel">Скасаваць</string>
|
||||
<string name="open_in_browser">Адкрыць у браўзеры</string>
|
||||
<string name="open_in_popup_mode">Адкрыць у асобным акне</string>
|
||||
<string name="open_in_browser">Адкрыць ў браўзеры</string>
|
||||
<string name="open_in_popup_mode">Адкрыць ў асобным акне</string>
|
||||
<string name="share">Падзяліцца</string>
|
||||
<string name="download">Спампаваць</string>
|
||||
<string name="controls_download_desc">Загрузка файла прамой трансляцыі</string>
|
||||
|
@ -37,12 +37,12 @@
|
|||
<string name="download_path_audio_summary">Загружаныя аўдыёфайлы захоўваюцца тут</string>
|
||||
<string name="download_path_audio_dialog_title">Абярыце тэчку загрузкі для аўдыёфайлаў</string>
|
||||
<string name="default_resolution_title">Разрознянне па змаўчанні</string>
|
||||
<string name="default_popup_resolution_title">Разрозненне усплываючага акна</string>
|
||||
<string name="default_popup_resolution_title">Разрозненне ўсплываючага акна</string>
|
||||
<string name="show_higher_resolutions_title">Высокія разрозненні</string>
|
||||
<string name="show_higher_resolutions_summary">Толькі некаторыя прылады могуць прайграваць відэа ў 2K/4K</string>
|
||||
<string name="play_with_kodi_title">Прайграць у Kodi</string>
|
||||
<string name="kore_not_found">Усталяваць адсутную праграму Kore\?</string>
|
||||
<string name="show_play_with_kodi_title">Паказаць опцыю \"Прайграць у Kodi\"</string>
|
||||
<string name="play_with_kodi_title">Прайграць ў Kodi</string>
|
||||
<string name="kore_not_found">Ўсталяваць адсутную праграму Kore?</string>
|
||||
<string name="show_play_with_kodi_title">Паказаць опцыю \"Прайграць ў Kodi\"</string>
|
||||
<string name="show_play_with_kodi_summary">Паказаць опцыю прайгравання відэа праз медыяцэнтр Kodi</string>
|
||||
<string name="play_audio">Аўдыё</string>
|
||||
<string name="default_audio_format_title">Фармат аўдыё па змаўчанні</string>
|
||||
|
@ -70,7 +70,7 @@
|
|||
<string name="resume_on_audio_focus_gain_title">Узнавіць прайграванне</string>
|
||||
<string name="resume_on_audio_focus_gain_summary">Працягваць прайграванне пасля перапынкаў (напрыклад, тэлефонных званкоў)</string>
|
||||
<string name="download_dialog_title">Загрузіць</string>
|
||||
<string name="show_next_and_similar_title">\"Наступнае\" и \"Прапанаванае\" відэа</string>
|
||||
<string name="show_next_and_similar_title">\"Наступнае\" і \"Прапанаванае\" відэа</string>
|
||||
<string name="show_hold_to_append_title">Паказаць падказку \"Утрымлівайце, каб паставіць у чаргу\"</string>
|
||||
<string name="show_hold_to_append_summary">Паказаць падказку пры націсканні фонавай або ўсплывальнай кнопкі ў відэа \"Падрабязнасці:\"</string>
|
||||
<string name="unsupported_url">URL не падтрымліваецца</string>
|
||||
|
@ -227,7 +227,7 @@
|
|||
\nПалітыка прыватнасці NewPipe падрабязна тлумачыць, якія дадзеныя адпраўляюцца і захоўваюцца пры адпраўцы справаздачы аб збоях.</string>
|
||||
<string name="read_privacy_policy">Прачытаць палітыку</string>
|
||||
<string name="app_license_title">Ліцэнзія NewPipe</string>
|
||||
<string name="app_license">NewPipe - гэта праграмнае забеспячэнне, свабоднае ад копілефта: вы можаце выкарыстоўваць, вывучаць, дзяліцца і паляпшаць яго па жаданні. У прыватнасці, вы можаце распаўсюджваць і/ці змяняць яго ў адпаведнасці з умовамі Агульнай грамадскай ліцэнзіі GNU, апублікаванай Фондам свабоднага праграмнага забеспячэння, альбо версіі 3 Ліцэнзіі, альбо (на ваш выбар) любой пазнейшай версіі.</string>
|
||||
<string name="app_license">NewPipe - гэта праграмнае забеспячэнне, свабоднае ад копілефта: вы можаце выкарыстоўваць, вывучаць, дзяліцца і паляпшаць яго па жаданні. Ў прыватнасці, вы можаце распаўсюджваць і/ці змяняць яго ў адпаведнасці з умовамі Агульнай грамадскай ліцэнзіі GNU, апублікаванай Фондам свабоднага праграмнага забеспячэння, альбо версіі 3 Ліцэнзіі, альбо (на ваш выбар) любой пазнейшай версіі.</string>
|
||||
<string name="read_full_license">Прачытаць ліцэнзію</string>
|
||||
<string name="title_activity_history">Гісторыя</string>
|
||||
<string name="action_history">Гісторыя</string>
|
||||
|
@ -253,8 +253,8 @@
|
|||
<string name="play_queue_remove">Выдаліць</string>
|
||||
<string name="play_queue_stream_detail">Падрабязнасці</string>
|
||||
<string name="play_queue_audio_settings">Налады аўдыё</string>
|
||||
<string name="hold_to_append">Утрымлівайце, каб дадаць у чаргу</string>
|
||||
<string name="start_here_on_background">Пачаць адсюль у фоне</string>
|
||||
<string name="hold_to_append">Утрымлівайце, каб дадаць ў чаргу</string>
|
||||
<string name="start_here_on_background">Пачаць адсюль ў фоне</string>
|
||||
<string name="start_here_on_popup">Пачніце гуляць ва ўсплываючым акне</string>
|
||||
<string name="drawer_open">Адкрыць бакавую панэль</string>
|
||||
<string name="drawer_close">Зачыніць бакавую панэль</string>
|
||||
|
@ -262,16 +262,16 @@
|
|||
<string name="preferred_open_action_settings_summary">Пры адкрыцці спасылкі на кантэнт — %s</string>
|
||||
<string name="video_player">Відэаплэер</string>
|
||||
<string name="background_player">Фонавы плэер</string>
|
||||
<string name="popup_player">Плэер у акне</string>
|
||||
<string name="popup_player">Аконны прайгравальнік</string>
|
||||
<string name="always_ask_open_action">Заўсёды пытацца</string>
|
||||
<string name="preferred_player_fetcher_notification_title">Атрыманне звестак…</string>
|
||||
<string name="preferred_player_fetcher_notification_message">Загрузка запытанага кантэнту</string>
|
||||
<string name="create_playlist">Стварыць плэйліст</string>
|
||||
<string name="rename_playlist">Перайменаваць</string>
|
||||
<string name="name">Імя</string>
|
||||
<string name="add_to_playlist">Дадаць у плэйліст</string>
|
||||
<string name="set_as_playlist_thumbnail">Усталяваць як мініяцюру плэйліста</string>
|
||||
<string name="bookmark_playlist">Дадаць плэйліст у закладкі</string>
|
||||
<string name="add_to_playlist">Дадаць ў плэйліст</string>
|
||||
<string name="set_as_playlist_thumbnail">Ўсталяваць як мініяцюру плэйліста</string>
|
||||
<string name="bookmark_playlist">Дадаць плэйліст ў закладкі</string>
|
||||
<string name="unbookmark_playlist">Выдаліць закладку</string>
|
||||
<string name="delete_playlist_prompt">Выдаліць плэйліст\?</string>
|
||||
<string name="playlist_creation_success">Плэйліст створаны</string>
|
||||
|
@ -289,7 +289,7 @@
|
|||
<string name="enable_disposed_exceptions_summary">Прымусова паведамляць пра недастаўляемыя Rx-выключэнні па-за фрагментам або жыццёвым цыкле пасля выдалення</string>
|
||||
<string name="import_title">Імпарт</string>
|
||||
<string name="import_from">Імпарт з</string>
|
||||
<string name="export_to">Экспарт у</string>
|
||||
<string name="export_to">Экспарт ў</string>
|
||||
<string name="import_ongoing">Імпарт…</string>
|
||||
<string name="export_ongoing">Экспарт…</string>
|
||||
<string name="import_file_title">Імпарт файла</string>
|
||||
|
@ -299,17 +299,17 @@
|
|||
<string name="import_youtube_instructions">Імпарт падпісак YouTube з Google Takeout:
|
||||
\n
|
||||
\n1. Перайдзіце па гэтым URL: %1$s
|
||||
\n2. Увайдзіце, калі вас папросяць
|
||||
\n2. Ўвайдзіце, калі вас папросяць
|
||||
\n3. Націсніце на «Усе дадзеныя ўключаны», затым на «Адмяніць выбар усіх», затым выберыце толькі «падпіскі» і націсніце «ОК»
|
||||
\n4. Націсніце на «Наступны крок», а затым на «Стварыць экспарт»
|
||||
\n5. Націсніце на кнопку «Спампаваць» пасля таго, як яна з\'явіцца
|
||||
\n6. Пстрыкніце ФАЙЛ ІМПАРТУВАЦЬ ніжэй і выберыце спампаваны файл .zip
|
||||
\n7. [Калі імпарт .zip не ўдаецца] Распакуйце файл .csv (звычайна ў раздзеле \"YouTube і YouTube Music/subscriptions/subscriptions.csv\"), націсніце ФАЙЛ ІМПАРТУВАЦЬ ніжэй і выберыце выняты файл CSV</string>
|
||||
<string name="import_soundcloud_instructions">Імпарт падпісак з SoundCloud набраўшы альбо URL, альбо ваш ID:
|
||||
\n
|
||||
\n1. Уключыце \"рэжым працоўнага стала\" у браўзэры (сайт недаступны на тэлефоне)
|
||||
\n2. Перайдзіце на: %1$s
|
||||
\n3. Увайдзіце, калі неабходна
|
||||
<string name="import_soundcloud_instructions">Імпарт падпісак з SoundCloud набраўшы альбо URL, альбо ваш ID:
|
||||
\n
|
||||
\n1. Ўключыце \"рэжым працоўнага стала\" ў браўзэры (сайт недаступны на тэлефоне)
|
||||
\n2. Перайдзіце на: %1$s
|
||||
\n3. Увайдзіце, калі неабходна
|
||||
\n4. Скапіруйце адрас з адраснага радка.</string>
|
||||
<string name="import_soundcloud_instructions_hint">вашID, soundcloud.com/вашID</string>
|
||||
<string name="import_network_expensive_warning">Гэтае дзеянне можа выклікаць вялікі расход трафіку.
|
||||
|
@ -322,7 +322,7 @@
|
|||
<string name="skip_silence_checkbox">Прапускаць цішыню</string>
|
||||
<string name="playback_step">Крок</string>
|
||||
<string name="playback_reset">Скід</string>
|
||||
<string name="start_accept_privacy_policy">У адпаведнасці з Агульным рэгламентам па абароне дадзеных ЕС (GDPR), звяртаем вашу ўвагу на палітыку прыватнасці NewPipe. Калі ласка, уважліва азнаёмцеся з ёй.
|
||||
<string name="start_accept_privacy_policy">Ў адпаведнасці з Агульным рэгламентам па абароне дадзеных ЕС (GDPR), звяртаем вашу ўвагу на палітыку прыватнасці NewPipe. Калі ласка, уважліва азнаёмцеся з ёй.
|
||||
\nВам неабходна прыняць яе ўмовы, каб адправіць нам справаздачу пра памылку.</string>
|
||||
<string name="accept">Прыняць</string>
|
||||
<string name="decline">Адмовіцца</string>
|
||||
|
@ -331,8 +331,8 @@
|
|||
<string name="minimize_on_exit_title">Пры згортванні плэера</string>
|
||||
<string name="minimize_on_exit_summary">Дзеянне пры пераключэнні са стандартнага плэера на іншае прыкладанне — %s</string>
|
||||
<string name="minimize_on_exit_none_description">Нічога не рабіць</string>
|
||||
<string name="minimize_on_exit_background_description">Згарнуць у фонавы плэер</string>
|
||||
<string name="minimize_on_exit_popup_description">Плэер у акне</string>
|
||||
<string name="minimize_on_exit_background_description">Згарнуць ў фонавы плэер</string>
|
||||
<string name="minimize_on_exit_popup_description">Плэер ў акне</string>
|
||||
<string name="unsubscribe">Адпісацца</string>
|
||||
<string name="tab_choose">Абярыце ўкладку</string>
|
||||
<string name="settings_category_updates_title">Абнаўленні</string>
|
||||
|
@ -356,14 +356,14 @@
|
|||
<string name="missions_header_finished">Скончана</string>
|
||||
<string name="missions_header_pending">У чарзе</string>
|
||||
<string name="paused">прыпынена</string>
|
||||
<string name="queued">у чарзе</string>
|
||||
<string name="queued">дададзены ў чаргу</string>
|
||||
<string name="post_processing">постапрацоўка</string>
|
||||
<string name="enqueue">Паставіць у чаргу</string>
|
||||
<string name="enqueue">Дадаць ў чаргу</string>
|
||||
<string name="permission_denied">Дзеянне забаронена сістэмай</string>
|
||||
<string name="download_failed">Памылка загрузкі</string>
|
||||
<string name="generate_unique_name">Стварыць унікальнае імя</string>
|
||||
<string name="overwrite">Перазапісаць</string>
|
||||
<string name="download_already_running">Загрузка з такім імем ужо выконваецца</string>
|
||||
<string name="download_already_running">Загрузка з такім імем ўжо выконваецца</string>
|
||||
<string name="show_error">Паказаць тэкст памылкі</string>
|
||||
<string name="error_path_creation">Немагчыма стварыць папку прызначэння</string>
|
||||
<string name="error_file_creation">Немагчыма стварыць файл</string>
|
||||
|
@ -377,7 +377,7 @@
|
|||
<string name="stop">Спыніць</string>
|
||||
<string name="max_retry_msg">Максімум спробаў</string>
|
||||
<string name="max_retry_desc">Колькасць спробаў перад адменай загрузкі</string>
|
||||
<string name="pause_downloads_on_mobile">Перапыніць у платных сетках</string>
|
||||
<string name="pause_downloads_on_mobile">Перапыніць ў платных сетках</string>
|
||||
<string name="pause_downloads_on_mobile_desc">Карысна пры пераключэнні на мабільную сетку, хоць некаторыя загрузкі не могуць быць прыпыненыя</string>
|
||||
<string name="events">Падзеі</string>
|
||||
<string name="conferences">Канферэнцыі</string>
|
||||
|
@ -394,12 +394,12 @@
|
|||
<string name="settings_category_clear_data_title">Ачысціць дадзеныя</string>
|
||||
<string name="watch_history_states_deleted">Пазіцыі прайгравання выдалены</string>
|
||||
<string name="missing_file">Файл перамешчаны ці выдалены</string>
|
||||
<string name="overwrite_unrelated_warning">Файл з такім імем ужо існуе</string>
|
||||
<string name="overwrite_finished_warning">Файл з такім імем ужо існуе</string>
|
||||
<string name="overwrite_unrelated_warning">Файл з такім імем ўжо існуе</string>
|
||||
<string name="overwrite_finished_warning">Файл з такім імем ўжо існуе</string>
|
||||
<string name="overwrite_failed">немагчыма перазапісаць файл</string>
|
||||
<string name="download_already_pending">У чарзе ўжо ёсць загрузка з такім імем</string>
|
||||
<string name="download_already_pending">Ў чарзе ўжо ёсць загрузка з такім імем</string>
|
||||
<string name="error_postprocessing_stopped">NewPipe была зачынена падчас працы над файлам</string>
|
||||
<string name="error_insufficient_storage">Скончылася вольнае месца на прыладзе</string>
|
||||
<string name="error_insufficient_storage_left">Скончылася вольнае месца на прыладзе</string>
|
||||
<string name="error_progress_lost">Прагрэс страчаны, так як файл быў выдалены</string>
|
||||
<string name="error_timeout">Час злучэння выйшла</string>
|
||||
<string name="confirm_prompt">Вы ўпэўнены\?</string>
|
||||
|
@ -408,8 +408,8 @@
|
|||
<string name="start_downloads">Пачаць загрузку</string>
|
||||
<string name="pause_downloads">Прыпыніць загрузку</string>
|
||||
<string name="downloads_storage_ask_title">Запытваць тэчку загрузкі</string>
|
||||
<string name="downloads_storage_ask_summary">Вам будзе прапанавана ўказаць месца захавання кожнай загрузкі.
|
||||
\nУключыце сістэмны выбарнік тэчкі (SAF), калі вы хочаце загружаць файлы на знешнюю SD-картку</string>
|
||||
<string name="downloads_storage_ask_summary">Вам будзе прапанавана указаць месца захавання кожнай загрузкі.
|
||||
\nЎключыце сістэмны выбарнік тэчкі (SAF), калі вы хочаце загружаць файлы на знешнюю SD-картку</string>
|
||||
<string name="downloads_storage_use_saf_title">Выкарыстоўвайце сродак выбару сістэмных тэчак (SAF)</string>
|
||||
<string name="downloads_storage_use_saf_summary">\'Storage Access Framework\' дазваляе загружаць на знешнюю SD-картку</string>
|
||||
<string name="drawer_header_description">Пераключыць службу, выбраную ў дадзены момант:</string>
|
||||
|
@ -426,7 +426,7 @@
|
|||
<string name="notification_action_1_title">Кнопка другога дзеяння</string>
|
||||
<string name="notification_action_0_title">Кнопка першага дзеяння</string>
|
||||
<string name="feed_groups_header_title">Групы каналаў</string>
|
||||
<string name="systems_language">Як у сістэме</string>
|
||||
<string name="systems_language">Як ў сістэме</string>
|
||||
<string name="app_language_title">Мова прылады</string>
|
||||
<string name="choose_instance_prompt">Выберыце экзэмпляр</string>
|
||||
<string name="delete_downloaded_files">Выдаліць загружаныя файлы</string>
|
||||
|
@ -442,15 +442,15 @@
|
|||
<string name="never">Ніколі</string>
|
||||
<string name="wifi_only">Толькі па Wi-Fi</string>
|
||||
<string name="show_original_time_ago_title">Паказаць арыгінальны час на элементах</string>
|
||||
<string name="unmute">Уключыць гук</string>
|
||||
<string name="unmute">Ўключыць гук</string>
|
||||
<string name="mute">Цішына</string>
|
||||
<string name="enqueue_stream">Дадаць у чаргу</string>
|
||||
<string name="enqueued">Даданае у чаргу</string>
|
||||
<string name="enqueue_stream">Дадаць ў чаргу</string>
|
||||
<string name="enqueued">Даданае ў чаргу</string>
|
||||
<string name="title_activity_play_queue">Чарга прайгравання</string>
|
||||
<string name="most_liked">Найбольш папулярнае</string>
|
||||
<string name="local">Лакальнае</string>
|
||||
<string name="recently_added">Нядаўна дададзенае</string>
|
||||
<string name="no_playlist_bookmarked_yet">Няма закладак у плейлісце</string>
|
||||
<string name="no_playlist_bookmarked_yet">Няма закладак ў плейлісце</string>
|
||||
<string name="select_a_playlist">Выберыце плэйліст</string>
|
||||
<string name="default_kiosk_page_summary">Кіёск па змаўчанні</string>
|
||||
<string name="done">Так</string>
|
||||
|
@ -478,7 +478,7 @@
|
|||
<string name="notification_action_4_title">Кнопка пятага дзеяння</string>
|
||||
<string name="notification_colorize_summary">Афарбоўваць апавяшчэнне асноўным колерам мініяцюры. Падтрымваецца не ўсімі прыладамі</string>
|
||||
<string name="notification_actions_at_most_three">У кампактным апавяшчэнні дасяжна не больш за тры дзеянні!</string>
|
||||
<string name="notification_actions_summary">Дзеянні можна змяніць, націснуўшы на іх. Адзначце не больш за трох для адлюстравання ў кампактным апавяшчэнні</string>
|
||||
<string name="notification_actions_summary">Адрэдагуйце кожнае дзеянне апавяшчэння, націснуўшы на яго. Выберыце да трох з іх, якія будуць адлюстроўвацца ў кампактным апавяшчэнні, выкарыстоўваючы сцяжкі справа.</string>
|
||||
<string name="unsupported_url_dialog_message">Не ўдалося распазнаць URL-адрас. Адкрыць у іншай праграме\?</string>
|
||||
<string name="settings_category_player_notification_title">Апавяшченне плэера</string>
|
||||
<string name="notifications">Апавяшчэнні</string>
|
||||
|
@ -497,9 +497,9 @@
|
|||
<string name="feed_group_dialog_empty_selection">Падпіскі не выбраны</string>
|
||||
<string name="feed_oldest_subscription_update">Апошняе абнаўленне: %s</string>
|
||||
<string name="auto_device_theme_title">Аўтаматычна (тэма прылады)</string>
|
||||
<string name="night_theme_summary">Выберыце ўлюбёную начную тэму - %s</string>
|
||||
<string name="night_theme_summary">Выберыце любімую начную тэму - %s</string>
|
||||
<string name="description_select_enable">Дазвол вылучэння тэксту ў апісанні</string>
|
||||
<string name="select_night_theme_toast">Ніжэй вы можаце абраць улюбёную начную тэму</string>
|
||||
<string name="select_night_theme_toast">Вы можаце выбраць сваю любімую начную тэму ніжэй</string>
|
||||
<string name="night_theme_available">Гэта опцыя даступна толькі тады, калі %s будзе выбранай тэмаю</string>
|
||||
<string name="download_has_started">Загрузка пачалась</string>
|
||||
<string name="notifications_disabled">Апавяшчэнні адключаныя</string>
|
||||
|
@ -519,7 +519,7 @@
|
|||
<string name="open_with">Адкрыць з дапамогай</string>
|
||||
<string name="night_theme_title">Начная тэма</string>
|
||||
<string name="open_website_license">Адкрыць вэб-сайт</string>
|
||||
<string name="description_select_note">Цяпер Вы можаце вылучаць тэкст у апісанні. Звярніце ўвагу, што ў рэжыме вылучэння старонка можа мігацець, а спасылкі могуць быць недаступныя для націскання.</string>
|
||||
<string name="description_select_note">Цяпер Вы можаце вылучаць тэкст ў апісанні. Звярніце ўвагу, што ў рэжыме вылучэння старонка можа мігацець, а спасылкі могуць быць недаступныя для націскання.</string>
|
||||
<string name="start_main_player_fullscreen_title">Запусціць галоўны прайгравальнік у поўнаэкранным рэжыме</string>
|
||||
<string name="show_channel_details">Паказаць дэталі канала</string>
|
||||
<string name="low_quality_smaller">Нізкая якасць (менш)</string>
|
||||
|
@ -574,14 +574,14 @@
|
|||
<item quantity="many">Выдалена %1$s зазагрузак</item>
|
||||
<item quantity="other">Выдалена %1$s зазагрузак</item>
|
||||
</plurals>
|
||||
<string name="delete_downloaded_files_confirm">Выдаліць усе загружаныя файлы з дыска\?</string>
|
||||
<string name="delete_downloaded_files_confirm">Выдаліць ўсе загружаныя файлы з дыска?</string>
|
||||
<plurals name="minutes">
|
||||
<item quantity="one">%d хвіліна</item>
|
||||
<item quantity="few">%d хвіліны</item>
|
||||
<item quantity="many">%d хвілінаў</item>
|
||||
<item quantity="other">%d хвілінаў</item>
|
||||
</plurals>
|
||||
<string name="progressive_load_interval_summary">Змяніць памер інтэрвалу загрузкі прагрэсіўнага змесціва (у цяперашні час %s). Меншае значэнне можа паскорыць іх першапачатковую загрузку</string>
|
||||
<string name="progressive_load_interval_summary">Змяніць памер інтэрвалу загрузкі прагрэсіўнага змесціва (ў цяперашні час %s). Меншае значэнне можа паскорыць іх першапачатковую загрузку</string>
|
||||
<string name="show_description_summary">Выключыце, каб схаваць апісанне відэа і дадатковую інфармацыю</string>
|
||||
<string name="local_search_suggestions">Прапановы лакальнага пошуку</string>
|
||||
<string name="settings_category_player_notification_summary">Наладзіць апавяшчэнне аб бягучым прайграванні патоку</string>
|
||||
|
@ -608,7 +608,7 @@
|
|||
<string name="msg_calculating_hash">Разлік хэша</string>
|
||||
<string name="recaptcha_solve">Вырашана</string>
|
||||
<string name="playlist_no_uploader">Створана аўтаматычна (запампавальнік не знойдзены)</string>
|
||||
<string name="duplicate_in_playlist">Плэйлісты, якія пазначаны шэрым, ужо ўтрымліваюць гэты элемент.</string>
|
||||
<string name="duplicate_in_playlist">Плэйлісты, якія пазначаны шэрым, ўжо ўтрымліваюць гэты элемент.</string>
|
||||
<plurals name="new_streams">
|
||||
<item quantity="one">%s новы стрым</item>
|
||||
<item quantity="few">%s новыя стрымы</item>
|
||||
|
@ -616,8 +616,8 @@
|
|||
<item quantity="other">%s новых стрымаў</item>
|
||||
</plurals>
|
||||
<string name="comments_tab_description">Каментарыі</string>
|
||||
<string name="enqueue_next_stream">У чаргу далей</string>
|
||||
<string name="enqueued_next">У чарзе наступны</string>
|
||||
<string name="enqueue_next_stream">Ў чаргу далей</string>
|
||||
<string name="enqueued_next">Ў чарзе наступны</string>
|
||||
<string name="loading_stream_details">Загрузка звестак аб стрыме…</string>
|
||||
<string name="processing_may_take_a_moment">Апрацоўка... Можа заняць некаторы час</string>
|
||||
<string name="playlist_add_stream_success_duplicate">Дублікат дададзены %d раз</string>
|
||||
|
@ -650,7 +650,7 @@
|
|||
<string name="checking_updates_toast">Праверка абнаўленняў…</string>
|
||||
<string name="remove_duplicates_title">Выдаліць дублікаты\?</string>
|
||||
<string name="remove_duplicates">Выдаліць дублікаты</string>
|
||||
<string name="remove_duplicates_message">Вы хочаце выдаліць усе паўтаральныя стрымы ў гэтым плэйлісце\?</string>
|
||||
<string name="remove_duplicates_message">Вы хочаце выдаліць ўсе паўтаральныя стрымы ў гэтым плэйлісце?</string>
|
||||
<string name="feed_new_items">Новыя элементы стужкі</string>
|
||||
<plurals name="feed_group_dialog_selection_count">
|
||||
<item quantity="one">%d выбраны</item>
|
||||
|
@ -670,7 +670,7 @@
|
|||
<string name="enable_streams_notifications_summary">Апавяшчаць аб новых стрымах з падпісак</string>
|
||||
<string name="streams_notifications_interval_title">Частата праверкі</string>
|
||||
<string name="streams_notifications_network_title">Патрабуецца падключэнне да сеткі</string>
|
||||
<string name="manual_update_title">Праверце наяўнасць абнаўленняў</string>
|
||||
<string name="check_for_updates">Праверце наяўнасць абнаўленняў</string>
|
||||
<string name="manual_update_description">Праверце новыя версіі ўручную</string>
|
||||
<string name="autoplay_summary">Аўтаматычны запуск прайгравання — %s</string>
|
||||
<string name="card">Картка</string>
|
||||
|
@ -687,12 +687,12 @@
|
|||
<string name="settings_category_feed_title">Стужка</string>
|
||||
<string name="feed_update_threshold_summary">Час пасля апошняга абнаўлення, перш чым падпіска лічыцца састарэлай — %s</string>
|
||||
<string name="feed_load_error">Памылка загрузкі стужкі</string>
|
||||
<string name="feed_load_error_terminated">Уліковы запіс аўтара быў спынены.
|
||||
\nNewPipe не зможа загрузіць гэты канал у будучыні.
|
||||
\nВы хочаце адмовіцца ад падпіскі на гэты канал\?</string>
|
||||
<string name="feed_load_error_terminated">Ўліковы запіс аўтара быў спынены.
|
||||
\nNewPipe не зможа загрузіць гэты канал ў будучыні.
|
||||
\nВы хочаце адмовіцца ад падпіскі на гэты канал?</string>
|
||||
<string name="feed_load_error_fast_unknown">Рэжым хуткай загрузкі стужкі не дае дадатковай інфармацыі аб гэтым.</string>
|
||||
<string name="feed_use_dedicated_fetch_method_title">Атрымлівайце са спецыяльнага канала, калі ён даступны</string>
|
||||
<string name="feed_use_dedicated_fetch_method_enable_button">Уключыць хуткі рэжым</string>
|
||||
<string name="feed_use_dedicated_fetch_method_enable_button">Ўключыць хуткі рэжым</string>
|
||||
<string name="metadata_category">Катэгорыя</string>
|
||||
<string name="metadata_tags">Тэгі</string>
|
||||
<string name="metadata_licence">Ліцэнзія</string>
|
||||
|
@ -700,7 +700,7 @@
|
|||
<string name="metadata_privacy_unlisted">Не ў спісе</string>
|
||||
<string name="metadata_privacy_private">Прыватная</string>
|
||||
<string name="enumeration_comma">,</string>
|
||||
<string name="toggle_all">Пераключыць усё</string>
|
||||
<string name="toggle_all">Пераключыць ўсё</string>
|
||||
<string name="streams_not_yet_supported_removed">Стрымы, якія яшчэ не падтрымліваюцца загрузчыкам, не адлюстроўваюцца</string>
|
||||
<string name="detail_sub_channel_thumbnail_view_description">Мініяцюра аватара канала</string>
|
||||
<string name="video_detail_by">Аўтар: %s</string>
|
||||
|
@ -729,7 +729,7 @@
|
|||
<string name="account_terminated">Уліковы запіс спынены</string>
|
||||
<string name="service_provides_reason">%s дае наступную прычыну:</string>
|
||||
<string name="featured">Рэкамендаваны</string>
|
||||
<string name="metadata_privacy_internal">Унутраная</string>
|
||||
<string name="metadata_privacy_internal">Ўнутраная</string>
|
||||
<string name="feed_show_watched">Цалкам прагледзеў</string>
|
||||
<string name="paid_content">Гэты кантэнт даступны толькі для аплачаных карыстальнікаў, таму NewPipe не можа яго трансляваць або спампоўваць.</string>
|
||||
<string name="feed_use_dedicated_fetch_method_summary">Даступны ў некаторых службах, звычайна нашмат хутчэй, але можа вяртаць абмежаваную колькасць элементаў і часта няпоўную інфармацыю (напрыклад, без працягласці, тыпу элемента, без актыўнага стану)</string>
|
||||
|
@ -739,7 +739,7 @@
|
|||
<string name="no_app_to_open_intent">Ніякая праграма на вашай прыладзе не можа адкрыць гэта</string>
|
||||
<string name="progressive_load_interval_exoplayer_default">Стандартнае значэнне ExoPlayer</string>
|
||||
<string name="feed_show_partially_watched">Часткова прагледжана</string>
|
||||
<string name="feed_use_dedicated_fetch_method_help_text">Як вы думаеце, загрузка корму адбываецца занадта павольна\? Калі так, паспрабуйце ўключыць хуткую загрузку (гэта можна змяніць у наладах або націснуўшы кнопку ніжэй).
|
||||
<string name="feed_use_dedicated_fetch_method_help_text">Як вы думаеце, загрузка корму адбываецца занадта павольна? Калі так, паспрабуйце ўключыць хуткую загрузку (гэта можна змяніць ў наладах або націснуўшы кнопку ніжэй).
|
||||
\n
|
||||
\nNewPipe прапануе дзве стратэгіі загрузкі корму:
|
||||
\n• Атрыманне ўсяго канала падпіскі павольнае, але поўнае.
|
||||
|
@ -775,8 +775,8 @@
|
|||
<string name="audio_track_type_original">арыгінальны</string>
|
||||
<string name="audio_track_type_dubbed">дубляваны</string>
|
||||
<string name="audio_track_type_descriptive">апісальны</string>
|
||||
<string name="audio_track_present_in_video">Гукавая дарожка ўжо павінна прысутнічаць у гэтай плыні</string>
|
||||
<string name="use_exoplayer_decoder_fallback_summary">Уключыце гэту опцыю, калі ў вас ёсць праблемы з ініцыялізацыяй дэкодэра, якая вяртаецца да дэкодэраў з больш нізкім прыярытэтам, калі ініцыялізацыя асноўных дэкодэраў не ўдаецца. Гэта можа прывесці да нізкай прадукцыйнасці прайгравання, чым пры выкарыстанні асноўных дэкодэраў</string>
|
||||
<string name="audio_track_present_in_video">Гукавая дарожка ўжо павінна прысутнічаць ў гэтай плыні</string>
|
||||
<string name="use_exoplayer_decoder_fallback_summary">Ўключыце гэту опцыю, калі ў вас ёсць праблемы з ініцыялізацыяй дэкодэра, якая вяртаецца да дэкодэраў з больш нізкім прыярытэтам, калі ініцыялізацыя асноўных дэкодэраў не ўдаецца. Гэта можа прывесці да нізкай прадукцыйнасці прайгравання, чым пры выкарыстанні асноўных дэкодэраў</string>
|
||||
<string name="settings_category_exoplayer_summary">Кіраванне некаторымі наладамі ExoPlayer. Каб гэтыя змены ўступілі ў сілу, патрабуецца перазапуск гульца</string>
|
||||
<string name="always_use_exoplayer_set_output_surface_workaround_summary">Гэты абыходны шлях вызваляе і паўторна стварае відэакодэкі, калі адбываецца змяненне паверхні, замест таго, каб усталёўваць паверхню непасрэдна для кодэка. ExoPlayer ужо выкарыстоўваецца на некаторых прыладах з гэтай праблемай, гэты параметр мае ўплыў толькі на прыладах з Android 6 і вышэй
|
||||
\n
|
||||
|
@ -820,7 +820,7 @@
|
|||
<string name="video_details_list_item">- %1$s: %2$s</string>
|
||||
<string name="main_tabs_position_summary">Перамясціць селектар галоўнай укладкі ўніз</string>
|
||||
<string name="no_live_streams">Няма жывых трансляцый</string>
|
||||
<string name="image_quality_summary">Выберыце якасць выявы і ці спампоўваць выявы ўвогуле, каб паменшыць выкарыстанне дадзеных і памяці. Змены ачышчаюць кэш малюнкаў як у памяці, так і на дыску - %s</string>
|
||||
<string name="image_quality_summary">Выберыце якасць выявы і ці спампоўваць выявы ўвогуле, каб паменшыць выкарыстанне дадзеных і памяці. Змены ачышчаюць кэш малюнкаў як ў памяці, так і на дыску - %s</string>
|
||||
<string name="play">Прайграць</string>
|
||||
<string name="more_options">Іншыя опцыі</string>
|
||||
<string name="metadata_thumbnails">Мініяцюры</string>
|
||||
|
@ -830,4 +830,14 @@
|
|||
<string name="channel_tab_channels">Каналы</string>
|
||||
<string name="previous_stream">Папярэдні стрым</string>
|
||||
<string name="channel_tab_livestreams">Жывая трансляцыя</string>
|
||||
<plurals name="replies">
|
||||
<item quantity="one">%s адказ</item>
|
||||
<item quantity="few">%s адказы</item>
|
||||
<item quantity="many">%s адказаў</item>
|
||||
<item quantity="other">%s адказаў</item>
|
||||
</plurals>
|
||||
<string name="show_more">Паказаць больш</string>
|
||||
<string name="show_less">Паказаць менш</string>
|
||||
<string name="notification_actions_summary_android13">Адрэдагуйце кожнае дзеянне апавяшчэння, націснуўшы на яго. Першыя тры дзеянні (прайграванне/паўза, папярэдняе і наступнае) задаюцца сістэмай і не могуць быць зменены.</string>
|
||||
<string name="error_insufficient_storage">Недастаткова вольнага месца на прыладзе</string>
|
||||
</resources>
|
|
@ -467,7 +467,7 @@
|
|||
<string name="choose_instance_prompt">Изберете инстанция</string>
|
||||
<string name="comments_are_disabled">Коментарите са изключени</string>
|
||||
<string name="main_page_content_summary">Кои раздели се показват на началната страница</string>
|
||||
<string name="error_insufficient_storage">Няма свободно място на устройството</string>
|
||||
<string name="error_insufficient_storage_left">Няма свободно място на устройството</string>
|
||||
<plurals name="seconds">
|
||||
<item quantity="one">%d секунда</item>
|
||||
<item quantity="other">%d секунди</item>
|
||||
|
@ -548,7 +548,7 @@
|
|||
<string name="recaptcha_cookies_cleared">Бисквитките от reCAPTCHA бяха почистени</string>
|
||||
<string name="checking_updates_toast">Проверяване за актуализации…</string>
|
||||
<string name="enumeration_comma">,</string>
|
||||
<string name="manual_update_title">Провери за актуализации</string>
|
||||
<string name="check_for_updates">Провери за актуализации</string>
|
||||
<string name="percent">Процент</string>
|
||||
<string name="unknown_quality">Неизвестно качество</string>
|
||||
<string name="unknown_format">Неизвестен формат</string>
|
||||
|
|
|
@ -165,7 +165,7 @@
|
|||
<string name="stop">বন্ধ করুন</string>
|
||||
<string name="delete_downloaded_files">ডাউনলোড করা ফাইলগুলো ডিলিট করুন</string>
|
||||
<string name="clear_download_history">ডাওন লোড ইতিহাস মুছুন</string>
|
||||
<string name="error_insufficient_storage">ডিভাইস এ স্পেস নেই</string>
|
||||
<string name="error_insufficient_storage_left">ডিভাইস এ স্পেস নেই</string>
|
||||
<string name="error_http_not_found">পাওয়া যায় নি</string>
|
||||
<string name="error_unknown_host">সার্ভার পাওয়া যায় নি</string>
|
||||
<string name="show_error">এরর দেখান</string>
|
||||
|
@ -297,7 +297,7 @@
|
|||
<string name="notification_scale_to_square_image_title">থাম্বনেল ১:১ অনুপাতে সেট করো</string>
|
||||
<string name="systems_language">সিস্টেম ডিফল্ট</string>
|
||||
<string name="bookmark_playlist">প্লেলিস্ট বুকমার্ক করুন</string>
|
||||
<string name="feed_use_dedicated_fetch_method_title">"যখন পর্যাপ্ত নিবেদিত ফিড থেকে ডাটা সংগ্রহ করুন"</string>
|
||||
<string name="feed_use_dedicated_fetch_method_title">যখন পর্যাপ্ত নিবেদিত ফিড থেকে ডাটা সংগ্রহ করুন</string>
|
||||
<string name="feed_update_threshold_option_always_update">সবসময় হালনগাদ করুন</string>
|
||||
<string name="feed_update_threshold_summary">শেষ হালনাগাদের পর একটি সাবস্ক্রিপশনের আগের সময় সেকেলে বিবেচিত — %s</string>
|
||||
<string name="feed_update_threshold_title">ফিড হালনাগাদ প্রবেশস্থল</string>
|
||||
|
@ -317,11 +317,11 @@
|
|||
<string name="feed_groups_header_title">চ্যানেল গ্রুপ</string>
|
||||
<plurals name="days">
|
||||
<item quantity="one">%d দিন</item>
|
||||
<item quantity="other">"%d দিন"</item>
|
||||
<item quantity="other">%d দিন</item>
|
||||
</plurals>
|
||||
<plurals name="hours">
|
||||
<item quantity="one">%d ঘন্টা</item>
|
||||
<item quantity="other">"%d ঘন্টা"</item>
|
||||
<item quantity="other">%d ঘন্টা</item>
|
||||
</plurals>
|
||||
<plurals name="minutes">
|
||||
<item quantity="one">%d মিনিট</item>
|
||||
|
@ -401,7 +401,7 @@
|
|||
<string name="default_content_country_title">কনটেন্টের জন্য পূর্বনির্ধারিত দেশ</string>
|
||||
<string name="external_player_unsupported_link_type">বাইরের প্লেয়ারসমূহ এ ধরনের লিঙ্কসমূহ সমর্থন করে না</string>
|
||||
<string name="msg_calculating_hash">হ্যাশ হিসাব করা হচ্ছে</string>
|
||||
<string name="manual_update_title">আপডেট চেক করো</string>
|
||||
<string name="check_for_updates">আপডেট চেক করো</string>
|
||||
<plurals name="watching">
|
||||
<item quantity="one">%s জন দেখছে</item>
|
||||
<item quantity="other">%s জন দেখছে</item>
|
||||
|
|
|
@ -141,7 +141,7 @@
|
|||
<string name="app_language_title">অ্যাপ এর ভাষা</string>
|
||||
<string name="stop">বন্ধ করুন</string>
|
||||
<string name="clear_download_history">ডাওন লোড ইতিহাস মুছুন</string>
|
||||
<string name="error_insufficient_storage">ডিভাইস এ স্পেস নেই</string>
|
||||
<string name="error_insufficient_storage_left">ডিভাইস এ স্পেস নেই</string>
|
||||
<string name="error_http_not_found">পাওয়া যায় নি</string>
|
||||
<string name="error_unknown_host">সার্ভার পাওয়া যায় নি</string>
|
||||
<string name="download_failed">ডাউন লোড হয় নি</string>
|
||||
|
@ -275,7 +275,7 @@
|
|||
<string name="website_encouragement">নিউ পাইপ ওয়েব সাইট এ যান বিস্তারিত বিবরণ ও খবর এর জন্য</string>
|
||||
<string name="more_than_100_videos">১০০+ ভিডিও</string>
|
||||
<plurals name="listening">
|
||||
<item quantity="one">"%s শ্রোতা"</item>
|
||||
<item quantity="one">%s শ্রোতা</item>
|
||||
<item quantity="other">%s শ্রোতা গন</item>
|
||||
</plurals>
|
||||
<string name="description_tab_description">বিবরণ</string>
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
<string name="stop">বন্ধ করুন</string>
|
||||
<string name="delete_downloaded_files">ডাউনলোড করা ফাইলগুলো ডিলিট করুন</string>
|
||||
<string name="clear_download_history">ডাওন লোড ইতিহাস মুছুন</string>
|
||||
<string name="error_insufficient_storage">ডিভাইস এ স্পেস নেই</string>
|
||||
<string name="error_insufficient_storage_left">ডিভাইস এ স্পেস নেই</string>
|
||||
<string name="error_http_not_found">পাওয়া যায় নি</string>
|
||||
<string name="error_unknown_host">সার্ভার পাওয়া যায় নি</string>
|
||||
<string name="show_error">এরর দেখান</string>
|
||||
|
@ -596,7 +596,7 @@
|
|||
<string name="detail_pinned_comment_view_description">পিনকৃত মন্তব্য</string>
|
||||
<string name="notifications">বিজ্ঞপ্তি</string>
|
||||
<string name="checking_updates_toast">হালনাগাদ দেখা হচ্ছে …</string>
|
||||
<string name="manual_update_title">হালনাগাদ আছে কিনা দেখো</string>
|
||||
<string name="check_for_updates">হালনাগাদ আছে কিনা দেখো</string>
|
||||
<string name="start_main_player_fullscreen_title">মূল প্লেয়ার ফুল স্ক্রীন এ শুরু করুন</string>
|
||||
<string name="feed_new_items">ধারার নতুন ভুক্তি</string>
|
||||
<string name="error_report_channel_name">ত্রুটি প্রতিবেদন এর বিজ্ঞপ্তি</string>
|
||||
|
|
|
@ -151,7 +151,7 @@
|
|||
<string name="invalid_file">El fitxer no existeix o bé no teniu permisos de lectura/escriptura</string>
|
||||
<string name="file_name_empty_error">El nom del fitxer no pot estar en blanc</string>
|
||||
<string name="error_occurred_detail">S\'ha produït un error: %1$s</string>
|
||||
<string name="error_report_button_text">Informeu de l\'error per correu electrònic</string>
|
||||
<string name="error_report_button_text">Informeu per correu electrònic</string>
|
||||
<string name="error_snackbar_message">S\'han produït alguns errors.</string>
|
||||
<string name="error_snackbar_action">Informe</string>
|
||||
<string name="what_device_headline">Informació:</string>
|
||||
|
@ -385,7 +385,7 @@
|
|||
<string name="enable_playback_resume_title">Reprèn la reproducció</string>
|
||||
<string name="overwrite_failed">No es pot sobreescriure el fitxer</string>
|
||||
<string name="download_already_pending">Hi ha una baixada pendent amb aquest nom</string>
|
||||
<string name="error_insufficient_storage">No hi ha espai disponible al dispositiu</string>
|
||||
<string name="error_insufficient_storage_left">No hi ha espai disponible al dispositiu</string>
|
||||
<string name="error_progress_lost">S\'ha perdut el progrés perquè s\'ha eliminat el fitxer</string>
|
||||
<string name="error_timeout">S\'ha excedit el temps d\'espera de la connexió</string>
|
||||
<string name="confirm_prompt">Esteu segurs que voleu esborrar el vostre historial de baixades o esborrar-ne tots els fitxers\?</string>
|
||||
|
@ -561,7 +561,7 @@
|
|||
<string name="notification_action_shuffle">Mescla</string>
|
||||
<string name="notification_action_repeat">Repeteix</string>
|
||||
<string name="notification_actions_at_most_three">El màxim d\'accions que poden aparèixer en una notificació compacta és de tres!</string>
|
||||
<string name="notification_actions_summary">Editeu cada acció de la notificació tocant el botó corresponent. Podeu seleccionar-ne fins a tres, que es mostraran a les notificacions en format compacte</string>
|
||||
<string name="notification_actions_summary">Editeu cada acció de la notificació tocant el botó corresponent. Podeu seleccionar-ne fins a tres, que es mostraran a les notificacions en format compacte.</string>
|
||||
<string name="notification_action_4_title">Cinquè botó d\'acció</string>
|
||||
<string name="notification_action_3_title">Quart botó d\'acció</string>
|
||||
<string name="notification_action_2_title">Tercer botó d\'acció</string>
|
||||
|
@ -640,7 +640,7 @@
|
|||
<string name="start_main_player_fullscreen_summary">Si la rotació automàtica està bloquejada, no inicieu vídeos al mini reproductor, sinó que aneu directament al mode de pantalla completa. Podeu accedir igualment al mini reproductor sortint de pantalla completa</string>
|
||||
<string name="error_report_channel_name">Notificació d\'informe d\'error</string>
|
||||
<string name="crash_the_player">Tancar abruptament el reproductor</string>
|
||||
<string name="manual_update_title">Comprovar si hi ha actualitzacions</string>
|
||||
<string name="check_for_updates">Comprovar si hi ha actualitzacions</string>
|
||||
<string name="manual_update_description">Comprovar manualment si hi ha noves versions</string>
|
||||
<plurals name="download_finished_notification">
|
||||
<item quantity="one">Baixada finalitzada</item>
|
||||
|
@ -692,9 +692,26 @@
|
|||
<string name="unknown_quality">Cualitat desconeguda</string>
|
||||
<string name="sort">Ordenar</string>
|
||||
<string name="settings_category_player_notification_summary">Configura la notificació de reproducció actual.</string>
|
||||
<string name="progressive_load_interval_summary">Canvia la mida de l\'interval de càrrega (actualment %s). Un valor inferior pot accelerar la càrrega inicial del vídeo. Els canvis requereixen un reinici del jugador.</string>
|
||||
<string name="progressive_load_interval_summary">Canvia la mida de l\'interval de càrrega en continguts progressius (actualment %s). Un valor inferior pot accelerar la càrrega inicial del vídeo.</string>
|
||||
<string name="ignore_hardware_media_buttons_title">Ignora els esdeveniments dels botons de reproducció físics</string>
|
||||
<string name="ignore_hardware_media_buttons_summary">Útil, per exemple, si feu servir uns auriculars amb els botons físicament trencats</string>
|
||||
<string name="left_gesture_control_summary">Trieu un gest per la part esquerra de la pantalla</string>
|
||||
<string name="progressive_load_interval_title">Mida de l\'interval de càrrega de reproducció</string>
|
||||
<string name="left_gesture_control_title">Acció de gest esquerra</string>
|
||||
<string name="notification_actions_summary_android13">Editeu cada acció de notificació de sota tocant-la. Les tres primeres accions (reproduir/pausa, anterior i següent) són establertes pel sistema i no es poden personalitzar.</string>
|
||||
<string name="right_gesture_control_summary">Tria un gest per a la meitat dreta del reproductor</string>
|
||||
<string name="right_gesture_control_title">Acció del gest dret</string>
|
||||
<string name="brightness">Brillantor</string>
|
||||
<string name="volume">Volum</string>
|
||||
<string name="none">Cap</string>
|
||||
<string name="main_tabs_position_summary">Mou el selector de pestanya principal a la part inferior</string>
|
||||
<string name="main_tabs_position_title">Posició de les pestanyes principals</string>
|
||||
<string name="prefer_descriptive_audio_title">Prefereix àudio descriptiu</string>
|
||||
<string name="prefer_original_audio_summary">Seleccioneu la pista d\'àudio original independentment de l\'idioma</string>
|
||||
<string name="prefer_original_audio_title">Prefereix l\'àudio original</string>
|
||||
<string name="fast_mode">Mode ràpid</string>
|
||||
<string name="loading_metadata_title">Carregant Metadades…</string>
|
||||
<string name="prefer_descriptive_audio_summary">Seleccioneu una pista d\'àudio amb descripcions per a persones amb discapacitat visual si està disponible</string>
|
||||
<string name="streams_notification_channel_name">Nous streams</string>
|
||||
<string name="streams_notification_channel_description">Notificacions sobre nous streams per a subscripcions</string>
|
||||
</resources>
|
|
@ -16,12 +16,12 @@
|
|||
<string name="privacy_policy_encouragement">پڕۆژەی نیوپایپ زانیارییە تایبەتییەکانت بە وردی دەپارێزێت. هەروەها بهرنامهكه هیچ زانایارییەکت بەبێ ئاگاداری تۆ بەکارنابات.
|
||||
\nسیاسەتی تایبەتی نیوپایپ بە وردی ڕوونکردنەوەت دەداتێ لەسەر ئەو زانیاریانەی وەریاندەگرێت و بەکاریاندەبات.</string>
|
||||
<string name="download_to_sdcard_error_message">ناتوانرێت لە بیرگەی دەرەکیدا داببەزێنرێت . شوێنی فۆڵدهری دابهزاندنەکان ڕێکبخرێتەوە؟</string>
|
||||
<string name="did_you_mean">ئایا مەبەستت ئهمهیه \"%1$s\"؟</string>
|
||||
<string name="did_you_mean">مەبەستت لە ئەمەیە ٪1$s ؟</string>
|
||||
<string name="feed_update_threshold_title">ماوەی نوێكردنهوهی فیید</string>
|
||||
<string name="grid">هێڵەکی</string>
|
||||
<string name="auto_queue_summary">بهردهوامبوون له (بهبێ دووبارهكردنهوه) نۆبهتی کارپێکەر بهپێی پهخشی هاوشێوه</string>
|
||||
<string name="enable_queue_limit">سنووردانانی نۆرەی دابەزاندن</string>
|
||||
<string name="error_insufficient_storage">بیرگەی ناوەکیت پڕ بووە</string>
|
||||
<string name="error_insufficient_storage_left">بیرگەی ناوەکیت پڕ بووە</string>
|
||||
<string name="subscribers_count_not_available">ژمارەی بەژداری نادیارە</string>
|
||||
<string name="overwrite_failed">ناتوانرێت لەسەر ئەو فایلهوه جێگیر بکرێت</string>
|
||||
<string name="tab_choose">پهڕه هەڵبژێرە</string>
|
||||
|
@ -33,7 +33,7 @@
|
|||
<string name="name">ناو</string>
|
||||
<string name="error_postprocessing_failed">چارەسەرکردن ههرهسی هێنا</string>
|
||||
<string name="minimize_on_exit_title">بچوکبوونەوە لەکاتی گۆڕینی بهرنامه</string>
|
||||
<string name="download_path_summary">فایلی ڤیدیۆ دابهزێنراوەکان لێرەدا هەڵدەگیرێن</string>
|
||||
<string name="download_path_summary">فایلی ڤیدیۆ داگیراوەکان لێرەدا هەڵدەگیرێن</string>
|
||||
<string name="export_data_summary">هەناردە کردنی مێژوو ، بەژدارییهكان ، خشتهلێدانهكان و ڕێكخستنهكان</string>
|
||||
<string name="use_inexact_seek_summary">بردنەپێشی ناتەواوی خێرا وا لە لێدەرەکە دەکات کە بەخێرایی شوێنەکە بگۆڕێت. بردنەپێشی ٥ یان ١٥ یان ٢٥ چرکەیی لەگەڵ ئەمەدا کارناکات</string>
|
||||
<string name="enable_disposed_exceptions_summary">سکاڵاکردن لەسەر نەگەیاندنی Rx ی پەسەندنەکرا لە دەرەوەی پارچە یان چالاکی لەدوای پوختەکردن</string>
|
||||
|
@ -90,7 +90,7 @@
|
|||
<string name="play_with_kodi_title">لێدان به Kodi</string>
|
||||
<string name="error_unable_to_load_comments">ناتوانرێت لێدوانەکان باربکرێن</string>
|
||||
<string name="peertube_instance_add_help">بەستەری دۆخ دابنێ</string>
|
||||
<string name="download_path_audio_summary">فایلی دەنگە دابهزێنراوەکان لێرەدا هەڵدەگیرێن</string>
|
||||
<string name="download_path_audio_summary">فایلی دەنگە داگیراوەکان لێرەدا هەڵدەگیرێن</string>
|
||||
<string name="error_snackbar_message">ببورە، هەندێك کێشە ڕوویدا.</string>
|
||||
<string name="export_to">هەناردە کردن بۆ</string>
|
||||
<string name="settings_category_player_behavior_title">ڕەفتار</string>
|
||||
|
@ -105,13 +105,13 @@
|
|||
<string name="unbookmark_playlist">لادانی نیشانهكراو</string>
|
||||
<string name="tab_licenses">مۆڵەتەکان</string>
|
||||
<string name="subscription_update_failed">ناتوانرێت بهژداریكردنهكه نوێبكرێتهوه</string>
|
||||
<string name="controls_background_title">پاشبنەما</string>
|
||||
<string name="controls_background_title">پشت شاشە</string>
|
||||
<string name="search_no_results">بێ ئەنجامه</string>
|
||||
<string name="localization_changes_requires_app_restart">زمان دەگۆڕدرێت لەدوای داگیرساندنەوەی بهرنامهكه</string>
|
||||
<string name="remove_watched">لادانی سەیرکراو</string>
|
||||
<string name="enable_playback_state_lists_summary">پیشاندانی نیشانەکەری شوێنی کارپێکەر لە خشتەکاندا</string>
|
||||
<string name="enable_playback_state_lists_title">شوێنەکان لە خشتەکاندا</string>
|
||||
<string name="subscribed_button_title">بهژداریت</string>
|
||||
<string name="subscribed_button_title">بهژداریتکرد</string>
|
||||
<string name="caption_setting_description">بەهۆی گۆڕانکاری لە شێوەی ژێرنووسکردنەکە. پێویستە بهرنامهكه دابگیرسێنیتهوه</string>
|
||||
<plurals name="feed_group_dialog_selection_count">
|
||||
<item quantity="one">%d دیار کراوه</item>
|
||||
|
@ -132,8 +132,8 @@
|
|||
<item quantity="one">%s بینراو</item>
|
||||
<item quantity="other">%s بینراوان</item>
|
||||
</plurals>
|
||||
<string name="pause">ڕاگرتن</string>
|
||||
<string name="download_path_audio_dialog_title">فۆڵدەری دابهزاندنی فایله دەنگییەکان هەڵبژێرە</string>
|
||||
<string name="pause">وەستاندن</string>
|
||||
<string name="download_path_audio_dialog_title">فۆڵدەری داگرتنی فایله دەنگییەکان هەڵبژێرە</string>
|
||||
<string name="feed_create_new_group_button_title">نوێ</string>
|
||||
<string name="clear_views_history_title">سڕینەوەی مێژووی سەیرکراو</string>
|
||||
<string name="enable_playback_resume_title">بەردەوام بوونی کارپێکەر</string>
|
||||
|
@ -158,7 +158,7 @@
|
|||
<string name="play_all">لێدانی گشتی</string>
|
||||
<string name="invalid_source">هەمان فایل/بابەت بوونی نییە</string>
|
||||
<string name="start">دەستپێکردن</string>
|
||||
<string name="subscribe_button_title">بهژداری</string>
|
||||
<string name="subscribe_button_title">بهژداریکردن</string>
|
||||
<string name="show_play_with_kodi_title">بژاردەی ”لێدان بە Kodi“ پیشانبدرێت</string>
|
||||
<string name="tab_subscriptions">بهژدارییهكان</string>
|
||||
<string name="blank_page_summary">پەڕەی بەتاڵ</string>
|
||||
|
@ -199,12 +199,12 @@
|
|||
<string name="detail_sub_channel_thumbnail_view_description">وێنۆچکەی سهروێنهی کەناڵ</string>
|
||||
<string name="import_settings">دەتەوێت ڕێکخستنەکانیش هاوردە بكرینهوه؟</string>
|
||||
<string name="no_player_found">هیچ لێدەرێکی ڤیدیۆیی نهدۆزرایهوه. دهتهوێت VLC دابمەزرێنیت؟</string>
|
||||
<string name="download_path_title">فۆڵدهری دابهزاندنی ڤیدیۆ</string>
|
||||
<string name="download_path_title">فۆڵدهری داگرتنی ڤیدیۆ</string>
|
||||
<string name="drawer_open">کردنەوەی پلیکانە</string>
|
||||
<string name="light_theme_title">ڕووناك</string>
|
||||
<string name="show_search_suggestions_summary">ئەو پێشنیازکراوانە هەڵبژێرە کە پیشان دەدرێن لەکاتی گەڕاندا</string>
|
||||
<string name="error_progress_lost">کردارەکە ههرهسی هێنا, چونکە ئەو فایله سڕاوەتەوە</string>
|
||||
<string name="controls_add_to_playlist_title">زیادکردن بۆ</string>
|
||||
<string name="controls_add_to_playlist_title">زیادی بکە بۆ</string>
|
||||
<string name="no_subscribers">بهژداری نییه</string>
|
||||
<string name="peertube_instance_url_title">دۆخی پێرتووب</string>
|
||||
<string name="playlist_creation_success">خشتەلێدان سازکرا</string>
|
||||
|
@ -214,7 +214,7 @@
|
|||
<string name="infinite_videos">∞ ڤیدیۆ</string>
|
||||
<string name="use_inexact_seek_title">بەکارهێنانی بردنەپێشی ناتەواوی خێرا</string>
|
||||
<string name="error_occurred_detail">هەڵەیەک ڕوویدا : %1$s</string>
|
||||
<string name="download_path_dialog_title">فۆڵدهری دابهزاندن بۆ فایلی ڤیدیۆکان هەڵبژێرە</string>
|
||||
<string name="download_path_dialog_title">فۆڵدهری داگرتن بۆ فایلی ڤیدیۆکان هەڵبژێرە</string>
|
||||
<string name="channel_created_by">ساز کراوه لەلایەن %s</string>
|
||||
<string name="users">بەکارهێنەران</string>
|
||||
<string name="content">بابەت</string>
|
||||
|
@ -228,7 +228,7 @@
|
|||
<string name="privacy_policy_title">سیاسەتی تایبەتی نیوپایپ</string>
|
||||
<string name="settings_category_downloads_title">دابهزاندن</string>
|
||||
<string name="feed_use_dedicated_fetch_method_disable_button">ناكاراکردنی دۆخی خێرا</string>
|
||||
<string name="open_in_browser">كردنهوه له وێبگهر</string>
|
||||
<string name="open_in_browser">ئەم بڕگەی پێڕستە ڤیدیۆیەک یان ستریمێکی دەنگی دەکاتەوە لە وێبگەڕێکدا</string>
|
||||
<string name="error_http_no_content">ڕاژەکە هیچ داتایەک نانێرێت</string>
|
||||
<string name="watch_history_states_deleted">شوێنی کارپێکراوەکان سڕانەوە</string>
|
||||
<string name="app_update_notification_channel_description">پەیامەکانی وەشانە نوێیەکانی نیوپایپ</string>
|
||||
|
@ -261,7 +261,7 @@
|
|||
<item quantity="other">%d ڕۆژان</item>
|
||||
</plurals>
|
||||
<string name="rename_playlist">ناولێنانهوه</string>
|
||||
<string name="download">دابهزاندن</string>
|
||||
<string name="download">داگرتن</string>
|
||||
<string name="ok">باشە</string>
|
||||
<string name="metadata_cache_wipe_title">سڕینهوهی پاشماوەی مێتاداتا</string>
|
||||
<string name="error_download_resource_gone">ناتوانرێت ئەمه داببهزێنرێتهوه</string>
|
||||
|
@ -326,7 +326,7 @@
|
|||
<string name="default_resolution_title">قهبارهی بنەڕەتی</string>
|
||||
<string name="minimize_on_exit_popup_description">بچووککردنەوە بۆ پەنجەرە</string>
|
||||
<string name="songs">گۆرانییەکان</string>
|
||||
<string name="controls_download_desc">دابهزاندنی فایلی پەخش</string>
|
||||
<string name="controls_download_desc">داگرتنی فایلی پەخش</string>
|
||||
<string name="list_view_mode">شێوازی پیشاندانی خشتە</string>
|
||||
<string name="peertube_instance_add_title">زیادکردنی دۆخ</string>
|
||||
<string name="accept">پەسەند</string>
|
||||
|
@ -342,7 +342,7 @@
|
|||
<string name="main_page_content">بابەتی پەڕەی سەرەکی</string>
|
||||
<string name="feed_group_dialog_select_subscriptions">دیار کردنی بەژدارییەکان</string>
|
||||
<string name="import_file_title">هاوردهكردنی فایل</string>
|
||||
<string name="download_path_audio_title">فۆڵدهری دابهزاندنی دهنگ</string>
|
||||
<string name="download_path_audio_title">فۆڵدهری داگرتنی دهنگ</string>
|
||||
<string name="use_external_video_player_summary">ههندێك له قهبارهكان دهنگیان تێدا نامێنێتهوه</string>
|
||||
<string name="events">ڕووداوەکان</string>
|
||||
<string name="detail_uploader_thumbnail_view_description">وێنۆچکەی کەسی بەرزکەرەوە</string>
|
||||
|
@ -390,7 +390,7 @@
|
|||
<string name="start_here_on_background">دەستپێکردنی لێدان لە پاشبنەماوە</string>
|
||||
<string name="msg_name">ناوفایل</string>
|
||||
<string name="set_as_playlist_thumbnail">دانان لەسەر وێنۆچکەی خشتەلێدان</string>
|
||||
<string name="title_activity_about">دەربارەی نیوپایپ</string>
|
||||
<string name="title_activity_about">دەربارەی NewPipe</string>
|
||||
<string name="add_to_playlist">زیادکردن بۆ خشتەلێدان</string>
|
||||
<string name="unknown_content">(نەزانراو)</string>
|
||||
<string name="app_language_title">زمانی بهرنامه</string>
|
||||
|
@ -469,7 +469,7 @@
|
|||
<string name="metadata_cache_wipe_summary">سڕینەوەی پاشماوەی هەموو ماڵپهڕهكان</string>
|
||||
<string name="kore_not_found">بەرنامەکە نهدۆزرایهوه. دابمهزرێت؟</string>
|
||||
<string name="peertube_instance_add_fail">ناتوانرێ پشتگیری دۆخەکە بکرێ</string>
|
||||
<string name="install">دامەزراندن</string>
|
||||
<string name="install">دابەزاندن</string>
|
||||
<string name="videos_string">ڤیدیۆکان</string>
|
||||
<string name="unsupported_url">بەستەرەکە پشتگیری نەکراوە</string>
|
||||
<string name="playback_pitch">قیڕ</string>
|
||||
|
@ -484,7 +484,7 @@
|
|||
<string name="enable_queue_limit_desc">لەیەک کاتدا تەنیا یەک بابەت دادەبەزێنرێت</string>
|
||||
<string name="restore_defaults_confirmation">دەتەوێت بگەڕێنرێتەوە بۆ شێوازی بنەڕەتی؟</string>
|
||||
<string name="pause_downloads">وەستاندنی دابەزاندنەکان</string>
|
||||
<string name="tab_about">دەربارە</string>
|
||||
<string name="tab_about">دەربارە و پرسیارەکان</string>
|
||||
<string name="show_comments_title">پیشاندانی لێدوانەکان</string>
|
||||
<string name="start_accept_privacy_policy">بۆ جێبەجێکردنی فرمانەکان لەگەڵ یاسای پاراستنی داتای گشتی ئەوروپیدا (GDPR) , ئێمە سەرنجت ڕادەکێشین بۆ سیاسەتە تایبەتییەکانی نیوپایپ. تکایە بەئاگادارییەوە بیخوێنەرهوە.
|
||||
\nپێویستە پهسهندی بکەیت بۆ ناردنی سکاڵاکانت.</string>
|
||||
|
@ -502,7 +502,7 @@
|
|||
<string name="feed_update_threshold_summary">کاتی دوای دواین نوێکردنەوە پێش بەژداربوون ڕەچاوکراوە — %s</string>
|
||||
<string name="download_to_sdcard_error_title">بیرگەی دەرەکی بەردەست نییە</string>
|
||||
<string name="enable_playback_resume_summary">گێڕانەوەی کارپێکەر بۆ شوێنی پێشووتر</string>
|
||||
<string name="cancel">پاشگهزبوونهوه</string>
|
||||
<string name="cancel">هەڵوەشاندنەوه</string>
|
||||
<string name="tracks">تراکەکان</string>
|
||||
<string name="play_queue_audio_settings">ڕێکخستنەکانی دەنگ</string>
|
||||
<string name="downloads_storage_ask_summary">پرست پێ دەکرێت بۆ شوێنی دابەزاندنی هەر بابەتێک.
|
||||
|
@ -541,7 +541,7 @@
|
|||
<string name="notification_action_shuffle">تێکەڵکردن</string>
|
||||
<string name="notification_action_repeat">دووبارە</string>
|
||||
<string name="notification_actions_at_most_three">دەتوانیت تا سێ كردار دیار بكهیت تا پیشان بدرێن له پەیامەکەدا!</string>
|
||||
<string name="notification_actions_summary">دهستكاری ههر یهكێك لهم كردارانهی خوارهوه بكه لهڕێگهی كرته لهسهریان. دهتوانیت تا زیاتر له سێ دانهیان ههڵبژێریت له ڕێگای چوارگۆشهكانی لای ڕاستهوهیان، تا پیشان بدرێن له پەیامەکاندا</string>
|
||||
<string name="notification_actions_summary">دەستکاریکردنی هەر کردارێکی ئاگادارکەرەوە لە خوارەوە بە دەستلێدان. ۳- دانە هەڵبژێرە لە ڕێگەی بەکارهێنانی سندوقەبچوکەکە لای ڕاستەوە نیشان دەدرێت</string>
|
||||
<string name="notification_action_4_title">پێنجهم كرداری دوگمه</string>
|
||||
<string name="notification_action_3_title">چوارهم كرداری دوگمه</string>
|
||||
<string name="notification_action_2_title">سێیهم كرداری دوگمه</string>
|
||||
|
@ -555,7 +555,7 @@
|
|||
<string name="show_meta_info_title">پیشاندانی زانیاری مێتا</string>
|
||||
<string name="show_description_summary">ناكارایبكه بۆ شاردنهوهی دیسکریپشن لهسهر ڤیدیۆ و زانیاری زیاتر</string>
|
||||
<string name="show_description_title">پیشاندانی دیسکریپشن</string>
|
||||
<string name="night_theme_title">ڕووكاری شهو</string>
|
||||
<string name="night_theme_title">ڕووكاری تاریک</string>
|
||||
<string name="notification_colorize_summary">ئهندرۆید ڕهنگی پەیام دڵخواز دهكات بهپێی ڕهنگی سهرهكی وێنۆچكهكه ( ڕهچاوی ئهوه بكه كه ئهم تایبهتمهندییه ههموو ئامێرێك ناگرێتهوه )</string>
|
||||
<string name="notification_colorize_title">ڕهنگكردنی پەیام</string>
|
||||
<string name="youtube_restricted_mode_enabled_summary">یوتوب ”دۆخی قهدهغهكراو” پێشكهش دهكات كه بابەتە نهشیاوهكان دهشارێتهوه</string>
|
||||
|
@ -582,7 +582,7 @@
|
|||
\nجا دهتهوێت بهژداری لابدهیت لهم كهناڵه؟</string>
|
||||
<string name="feed_load_error_account_info">ناتوانرێت فیید باربکرێت تا ً`%s` .</string>
|
||||
<string name="feed_load_error">ههڵه له باركردنی فیید</string>
|
||||
<string name="disable_media_tunneling_summary">ئهم تایبهتمهندییه كارابكه گهر ڕوونمای ڕهش یاخوود جامبوونی کارپێکەرت ئهزموون كرد</string>
|
||||
<string name="disable_media_tunneling_summary">ئەگەر تووشی شاشەی ڕەش یان لکەلکە بوویت لە کاتی پەخشکردنی ڤیدیۆدا، تونێلکردنی میدیا لەکاربخە.</string>
|
||||
<string name="metadata_privacy_internal">ناوەکی</string>
|
||||
<string name="metadata_privacy_private">تایبەتی</string>
|
||||
<string name="metadata_privacy_unlisted">خشتەنەکراو</string>
|
||||
|
@ -658,18 +658,18 @@
|
|||
<string name="error_report_notification_title">نیوپایپ تووشی کێشەیەک بوو ، کرتە بکە بۆ سکاڵاکردن</string>
|
||||
<string name="show_crash_the_player_title">پیشاندانی ”کڕاش کردنی لێدەرەکە“</string>
|
||||
<string name="create_error_notification">سازاندنی پەیامی کێشەیەک</string>
|
||||
<string name="manual_update_title">پشکنین بۆ نوێکردنەوە</string>
|
||||
<string name="check_for_updates">پشکنین بۆ نوێکردنەوە</string>
|
||||
<string name="error_report_channel_name">کێشە لە سکاڵا کردنی پەیام</string>
|
||||
<string name="error_report_channel_description">پەیامەکانی سکاڵاکردن لە کێشەکان</string>
|
||||
<string name="feed_new_items">بابەتە نوێیەکانی فیید</string>
|
||||
<string name="detail_pinned_comment_view_description">لێدوانی هەڵواسراو</string>
|
||||
<string name="crash_the_player">کڕاش کردنی لێدەر</string>
|
||||
<string name="show_error_snackbar">پیشاندانی هەڵەی سناکباڕ</string>
|
||||
<string name="no_appropriate_file_manager_message">هیچ ڕێکخەرێکی فایلی گونجاو نەدۆزرایەوە بۆ ئەم کردارە.
|
||||
\nتکایە ڕێکخەری فایلییەک دابمەزرێنە لۆ هەوڵدانی ناکاراکردنی \'%s\' لە ڕێکخستنەکانی دابەزاندندا.</string>
|
||||
<string name="no_appropriate_file_manager_message">هیچ FileManager پەڕگەی گونجاو بۆ ئەم کردارە نەدۆزراوەتەوە.
|
||||
\nتکایە بەڕێوەبەری پەڕگەیەک دابمەزرێنە یان هەوڵبدە \'%s\' لە Settings بڕۆ Download لەکاربخە</string>
|
||||
<string name="leak_canary_not_available">LeakCanary بەردەست نییە</string>
|
||||
<string name="no_appropriate_file_manager_message_android_10">هیچ ڕێکخەرێکی فایلی گونجاو نەدۆزرایەوە بۆ ئەم کردارە.
|
||||
\nتکایە ڕێکخەرێکی فایلی دابمەزرێنە کە گونجاوبێت لەگەڵ دەسەڵاتی گەیشتن بە بیرگە.</string>
|
||||
<string name="no_appropriate_file_manager_message_android_10">هیچ FileManager گونجاو نەدۆزرایەوە بۆ ئەم کردارە.
|
||||
\nتکایە FileManager دابمەزرێنە کە گونجاوبێت لەگەڵ دەسەڵاتی گەیشتن بە بیرگە.</string>
|
||||
<string name="check_new_streams">پشکنین کردن بۆ پەخشی نوێ</string>
|
||||
<string name="enable_streams_notifications_title">پەیامەکانی پەخشە نوێیەکان</string>
|
||||
<string name="enable_streams_notifications_summary">پەیام بکرێم لەکاتی هەبوونی پەخشی نوێی بەژدارییەکان</string>
|
||||
|
@ -694,4 +694,12 @@
|
|||
<string name="percent">لەسەدا</string>
|
||||
<string name="semitone">نیمچەتەن</string>
|
||||
<string name="progressive_load_interval_exoplayer_default">بنەڕەتی ExoPlayer</string>
|
||||
<string name="prefer_original_audio_summary">دیاریکردنی تراکی دەنگی ئەسڵی بێ گوێدانە زمانەکە</string>
|
||||
<string name="notification_actions_summary_android13">دەستکاریکردنی هەر کردارێکی ئاگادارکەرەوە لە خوارەوە بە دەستلێدان. یەکەم سێ کردار (لێدان/وەستان، پێشوو و دواتر) لەلایەن سیستەمەکەوە دانراوە و ناتوانرێت دەستکاری بکرێت.</string>
|
||||
<string name="prefer_descriptive_audio_title">پەسەند کردنی دەنگی وەسفکراو</string>
|
||||
<string name="progressive_load_interval_summary">گۆڕینی قەبارەی ماوەی لۆد لەسەر ناوەڕۆکی پێشکەوتوو (ئێستا ٪s). بەهایەکی کەمتر لەوانەیە بارکردنی سەرەتا خێراتر بکات</string>
|
||||
<string name="prefer_original_audio_title">پەسەندکردنی دەنگی ئەسڵی</string>
|
||||
<string name="ignore_hardware_media_buttons_summary">بەسوودە، بۆ نموونە، ئەگەر هێدسێتێک بەکاربهێنیت لەگەڵ دوگمەی فیزیکی شکاو</string>
|
||||
<string name="progressive_load_interval_title">قەبارەی نێوان بارکردنی پەخشکردن</string>
|
||||
<string name="ignore_hardware_media_buttons_title">دوگمەی ڕووداوەکانی میدیای هاردوێر بەجێبهێڵە</string>
|
||||
</resources>
|
|
@ -396,7 +396,7 @@
|
|||
<string name="overwrite_failed">soubor nelze přepsat</string>
|
||||
<string name="download_already_pending">Soubor s tímto názvem již čeká na stažení</string>
|
||||
<string name="error_postprocessing_stopped">NewPipe byl ukončen v průběhu zpracovávání souboru</string>
|
||||
<string name="error_insufficient_storage">V zařízení nezbývá žádné místo</string>
|
||||
<string name="error_insufficient_storage_left">V zařízení nezbývá žádné místo</string>
|
||||
<string name="error_progress_lost">Postup ztracen, protože soubor byl smazán</string>
|
||||
<string name="confirm_prompt">Jste si jisti smazáním své historie stahování nebo smazáním všech stažených souborů\?</string>
|
||||
<string name="enable_queue_limit">Omezit frontu stahování</string>
|
||||
|
@ -554,7 +554,7 @@
|
|||
<string name="notification_action_shuffle">Promíchat</string>
|
||||
<string name="notification_action_repeat">Opakovat</string>
|
||||
<string name="notification_actions_at_most_three">Do kompaktního oznámení lze vybrat nejvíce tři akce!</string>
|
||||
<string name="notification_actions_summary">Upravte každou akci oznámení níže poklepáním. Pomocí zaškrtávacích políček vpravo vyberte až tři z nich, které se mají zobrazit v kompaktním oznámení</string>
|
||||
<string name="notification_actions_summary">Upravte každou akci oznámení níže poklepáním. Pomocí zaškrtávacích políček vpravo vyberte až tři z nich, které se mají zobrazit v kompaktním oznámení.</string>
|
||||
<string name="notification_action_4_title">Páté akční tlačítko</string>
|
||||
<string name="notification_action_3_title">Čtvrté akční tlačítko</string>
|
||||
<string name="notification_action_2_title">Třetí akční tlačítko</string>
|
||||
|
@ -665,7 +665,7 @@
|
|||
<string name="start_main_player_fullscreen_title">Spustit hlavní přehrávač na celé obrazovce</string>
|
||||
<string name="processing_may_take_a_moment">Zpracovávám... může trvat moment</string>
|
||||
<string name="manual_update_description">Ručně zkontrolovat zda je k dispozici nová verze</string>
|
||||
<string name="manual_update_title">Kontrola aktualizací</string>
|
||||
<string name="check_for_updates">Kontrola aktualizací</string>
|
||||
<string name="error_report_notification_title">NewPipe narazil na problém, klikněte pro nahlášení</string>
|
||||
<string name="error_report_notification_toast">Došlo k chybě, více v oznámení</string>
|
||||
<string name="create_error_notification">Vytvořit oznámení o chybě</string>
|
||||
|
@ -819,4 +819,23 @@
|
|||
<string name="channel_tab_channels">Kanály</string>
|
||||
<string name="previous_stream">Předchozí stream</string>
|
||||
<string name="channel_tab_livestreams">Živě</string>
|
||||
<plurals name="replies">
|
||||
<item quantity="one">%s odpověď</item>
|
||||
<item quantity="few">%s odpovědi</item>
|
||||
<item quantity="other">%s odpovědí</item>
|
||||
</plurals>
|
||||
<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>
|
File diff suppressed because it is too large
Load Diff
|
@ -104,7 +104,7 @@
|
|||
<string name="popup_remember_size_pos_summary">Letzte Größe und Position des Pop-ups merken</string>
|
||||
<string name="show_search_suggestions_title">Suchvorschläge</string>
|
||||
<string name="show_search_suggestions_summary">Vorschläge auswählen, die bei der Suche angezeigt werden sollen</string>
|
||||
<string name="clear">Löschen</string>
|
||||
<string name="clear">löschen</string>
|
||||
<string name="best_resolution">Beste Auflösung</string>
|
||||
<string name="title_activity_about">Über NewPipe</string>
|
||||
<string name="tab_licenses">Lizenzen</string>
|
||||
|
@ -405,7 +405,7 @@
|
|||
<string name="overwrite_failed">Datei kann nicht überschrieben werden</string>
|
||||
<string name="download_already_pending">Es ist ein ausstehender Download mit diesem Namen vorhanden</string>
|
||||
<string name="error_postprocessing_stopped">NewPipe wurde während der Verarbeitung der Datei geschlossen</string>
|
||||
<string name="error_insufficient_storage">Kein Speicherplatz mehr auf dem Gerät</string>
|
||||
<string name="error_insufficient_storage_left">Kein Speicherplatz mehr auf dem Gerät</string>
|
||||
<string name="error_progress_lost">Vorgang abgebrochen, da die Datei gelöscht wurde</string>
|
||||
<string name="confirm_prompt">Möchtest du deinen Downloadverlauf oder alle heruntergeladenen Dateien löschen\?</string>
|
||||
<string name="enable_queue_limit">Downloadwarteschlange begrenzen</string>
|
||||
|
@ -425,7 +425,7 @@
|
|||
<string name="no_one_watching">Niemand schaut zu</string>
|
||||
<plurals name="watching">
|
||||
<item quantity="one">%s Zuschauer</item>
|
||||
<item quantity="other">%s Zuschauer</item>
|
||||
<item quantity="other">%s Zuschauende</item>
|
||||
</plurals>
|
||||
<string name="no_one_listening">Niemand hört zu</string>
|
||||
<plurals name="listening">
|
||||
|
@ -541,7 +541,7 @@
|
|||
<string name="wifi_only">Nur über WLAN</string>
|
||||
<string name="never">Nie</string>
|
||||
<string name="notification_actions_at_most_three">Du kannst maximal drei Aktionen auswählen, die in der Kompaktbenachrichtigung angezeigt werden sollen!</string>
|
||||
<string name="notification_actions_summary">Bearbeite jede Benachrichtigungsaktion unten, indem du darauf tippst. Wähle mithilfe der Kontrollkästchen rechts bis zu drei aus, die in der Kompaktbenachrichtigung angezeigt werden sollen</string>
|
||||
<string name="notification_actions_summary">Bearbeite jede Benachrichtigungsaktion unten, indem du auf sie tippst. Wähle mithilfe der Kontrollkästchen rechts bis zu drei aus, die in der Kompaktbenachrichtigung angezeigt werden sollen.</string>
|
||||
<string name="unsupported_url_dialog_message">Konnte die angegebene URL nicht erkennen. Mit einer anderen Anwendung öffnen\?</string>
|
||||
<string name="notification_action_4_title">Fünfte Aktionstaste</string>
|
||||
<string name="notification_action_3_title">Vierte Aktionstaste</string>
|
||||
|
@ -655,7 +655,7 @@
|
|||
<string name="enqueued_next">Als Nächstes eingereiht</string>
|
||||
<string name="enqueue_next_stream">Als Nächstes in Wiedergabe einreihen</string>
|
||||
<string name="processing_may_take_a_moment">Verarbeite … Kann einen Moment dauern</string>
|
||||
<string name="manual_update_title">Nach Aktualisierungen suchen</string>
|
||||
<string name="check_for_updates">Nach Aktualisierungen suchen</string>
|
||||
<string name="checking_updates_toast">Suche nach Aktualisierungen …</string>
|
||||
<string name="manual_update_description">Manuelle Prüfung auf neue Versionen</string>
|
||||
<string name="feed_new_items">Neue Feed-Elemente</string>
|
||||
|
@ -806,4 +806,22 @@
|
|||
<string name="share_playlist">Wiedergabeliste teilen</string>
|
||||
<string name="share_playlist_with_titles_message">Teile die Wiedergabeliste mit Details wie dem Namen der Wiedergabeliste und den Videotiteln oder als einfache Liste von Video-URLs</string>
|
||||
<string name="video_details_list_item">- %1$s: %2$s</string>
|
||||
<plurals name="replies">
|
||||
<item quantity="one">%s Antwort</item>
|
||||
<item quantity="other">%s Antworten</item>
|
||||
</plurals>
|
||||
<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>
|
|
@ -395,7 +395,7 @@
|
|||
<string name="overwrite_failed">δεν είναι δυνατή η αντικατάσταση του αρχείου</string>
|
||||
<string name="download_already_pending">Υπάρχει μια εκκρεμής λήψη με αυτό το όνομα</string>
|
||||
<string name="error_postprocessing_stopped">Το NewPipe τερματίστηκε ενώ επεξεργάζονταν το αρχείο</string>
|
||||
<string name="error_insufficient_storage">Δεν υπάρχει αρκετός χώρος στη συσκευή</string>
|
||||
<string name="error_insufficient_storage_left">Δεν υπάρχει αρκετός χώρος στη συσκευή</string>
|
||||
<string name="error_progress_lost">Η πρόοδος χάθηκε, επειδή το αρχείο διαγράφηκε</string>
|
||||
<string name="error_timeout">Λήξη χρονικού ορίου σύνδεσης</string>
|
||||
<string name="confirm_prompt">Θέλετε να διαγράψετε το ιστορικό λήψεων σας ή να διαγράψετε όλα τα αρχεία που έχετε λάβει;</string>
|
||||
|
@ -482,7 +482,7 @@
|
|||
<string name="notification_action_shuffle">Ανάμιξη</string>
|
||||
<string name="notification_action_repeat">Επανάληψη</string>
|
||||
<string name="notification_actions_at_most_three">Μπορείτε να επιλέξετε το πολύ τρεις ενέργειες για εμφάνιση στη σύντομη ειδοποίηση!</string>
|
||||
<string name="notification_actions_summary">Επεξεργαστείτε κάθε ενέργεια ειδοποίησης παρακάτω πατώντας πάνω της. Επιλέξτε έως και τρεις από αυτές για να εμφανίζονται στη σύντομη ειδοποίηση, χρησιμοποιώντας τα πλαίσια ελέγχου στα δεξιά</string>
|
||||
<string name="notification_actions_summary">Επεξεργαστείτε κάθε ενέργεια ειδοποίησης παρακάτω πατώντας πάνω της. Επιλέξτε έως και τρεις από αυτές για να εμφανίζονται στη σύντομη ειδοποίηση, χρησιμοποιώντας τα πλαίσια ελέγχου στα δεξιά.</string>
|
||||
<string name="notification_action_4_title">Κουμπί πέμπτης ενέργειας</string>
|
||||
<string name="notification_action_3_title">Κουμπί τέταρτης ενέργειας</string>
|
||||
<string name="notification_action_2_title">Κουμπί τρίτης ενέργειας</string>
|
||||
|
@ -595,7 +595,7 @@
|
|||
<string name="auto_device_theme_title">Αυτόματο (θέμα συσκευής)</string>
|
||||
<string name="night_theme_title">Νυχτερινό θέμα</string>
|
||||
<string name="show_channel_details">Εμφάνιση λεπτομερειών καναλιού</string>
|
||||
<string name="disable_media_tunneling_summary">Απενεργοποιήστε το media tunneling, αν εμφανίζεται μαύρη οθόνη ή διακοπτόμενος ήχος κατά την αναπαραγωγή βίντεο</string>
|
||||
<string name="disable_media_tunneling_summary">Απενεργοποιήστε το media tunneling, αν παρατηρείτε μαύρη οθόνη ή διακοπές κατά την αναπαραγωγή βίντεο.</string>
|
||||
<string name="disable_media_tunneling_title">Απενεργοποίηση media tunneling</string>
|
||||
<string name="metadata_privacy_internal">Εσωτερικό</string>
|
||||
<string name="metadata_privacy_private">Ιδιωτικό</string>
|
||||
|
@ -654,7 +654,7 @@
|
|||
<string name="processing_may_take_a_moment">Επεξεργασία... Μπορεί να πάρει λίγο χρόνο</string>
|
||||
<string name="checking_updates_toast">Έλεγχος αναβάθμισης…</string>
|
||||
<string name="manual_update_description">Χειροκίνητος έλεγχος για νέα έκδοση</string>
|
||||
<string name="manual_update_title">Έλεγχος αναβάθμισης</string>
|
||||
<string name="check_for_updates">Έλεγχος αναβάθμισης</string>
|
||||
<string name="feed_new_items">Νέα αντικείμενα τροφοδοσίας</string>
|
||||
<string name="show_crash_the_player_title">Εμφάνιση «Κατάρρευσης αναπαραγωγέα»</string>
|
||||
<string name="show_crash_the_player_summary">Εμφανίζει μια επιλογή κατάρρευσης κατά τη χρήση του αναπαραγωγέα</string>
|
||||
|
@ -767,7 +767,7 @@
|
|||
<string name="metadata_subscribers">Συνδρομητές</string>
|
||||
<string name="show_channel_tabs_summary">Ποιες καρτέλες εμφανίζονται στις σελίδες των καναλιών</string>
|
||||
<string name="show_channel_tabs">Καρτέλες καναλιών</string>
|
||||
<string name="channel_tab_shorts">Shorts</string>
|
||||
<string name="channel_tab_shorts">Σύντομα</string>
|
||||
<string name="feed_fetch_channel_tabs">Λήψη καρτελών καναλιών</string>
|
||||
<string name="channel_tab_about">Σχετικά</string>
|
||||
<string name="channel_tab_albums">Άλμπουμ</string>
|
||||
|
@ -806,4 +806,22 @@
|
|||
<string name="share_playlist">Κοινοποίηση λίστας</string>
|
||||
<string name="share_playlist_with_titles_message">Μοιραστείτε τη λίστα αναπαραγωγής με λεπτομέρειες όπως το όνομα της λίστας αναπαραγωγής και τους τίτλους βίντεο ή ως μια απλή λίστα διευθύνσεων URL βίντεο</string>
|
||||
<string name="video_details_list_item">- %1$s: %2$s</string>
|
||||
<plurals name="replies">
|
||||
<item quantity="one">%s απάντηση</item>
|
||||
<item quantity="other">%s απαντήσεις</item>
|
||||
</plurals>
|
||||
<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>
|
|
@ -406,7 +406,7 @@
|
|||
<string name="overwrite_finished_warning">Elŝutita dosieron kun ĉi tiu nomo jam ekzistas</string>
|
||||
<string name="download_already_pending">Estas pritraktata elŝuto kun ĉi tiu nomo</string>
|
||||
<string name="error_postprocessing_stopped">NewPipe estis fermita dum laborante sur la dosiero</string>
|
||||
<string name="error_insufficient_storage">Neniu spaco havebla sur la aparato</string>
|
||||
<string name="error_insufficient_storage_left">Neniu spaco havebla sur la aparato</string>
|
||||
<string name="error_progress_lost">Progreso perdita, ĉar la dosiero estis forviŝita</string>
|
||||
<string name="error_timeout">Eltempiĝo de Konekto</string>
|
||||
<string name="drawer_header_description">Ŝangi la servon, nuntempe elektita:</string>
|
||||
|
@ -610,4 +610,5 @@
|
|||
<string name="related_items_tab_description">Rilatajn erojn</string>
|
||||
<string name="recaptcha_solve">Solvi</string>
|
||||
<string name="msg_failed_to_copy">Malsukcesis kopii al la tondujo</string>
|
||||
<string name="downloads_storage_ask_summary_no_saf_notice">Oni petos al vi kien salvi ĉiujn elŝutojn</string>
|
||||
</resources>
|
|
@ -47,7 +47,7 @@
|
|||
<string name="detail_uploader_thumbnail_view_description">Miniatura del avatar del usuario</string>
|
||||
<string name="content">Contenido</string>
|
||||
<string name="show_age_restricted_content_title">Mostrar contenido con restricción de edad</string>
|
||||
<string name="main_bg_subtitle">Pulsa la lupa para empezar.</string>
|
||||
<string name="main_bg_subtitle">Toca la lupa para empezar.</string>
|
||||
<string name="duration_live">En directo</string>
|
||||
<string name="downloads">Descargas</string>
|
||||
<string name="downloads_title">Descargas</string>
|
||||
|
@ -376,7 +376,7 @@
|
|||
<string name="error_http_not_found">No encontrado</string>
|
||||
<string name="error_postprocessing_failed">Falló el posprocesamiento</string>
|
||||
<string name="error_postprocessing_stopped">NewPipe se cerró mientras se trabajaba en el archivo</string>
|
||||
<string name="error_insufficient_storage">No hay suficiente espacio disponible en el dispositivo</string>
|
||||
<string name="error_insufficient_storage_left">No hay suficiente espacio disponible en el dispositivo</string>
|
||||
<string name="error_progress_lost">Progreso perdido, porque el archivo fue borrado</string>
|
||||
<string name="error_timeout">El tiempo de conexión expiro</string>
|
||||
<string name="error_download_resource_gone">No se puede recuperar esta descarga</string>
|
||||
|
@ -558,7 +558,7 @@
|
|||
<string name="notification_action_buffering">Almacenar en memoria (búfer)</string>
|
||||
<string name="notification_action_repeat">Repetir</string>
|
||||
<string name="notification_actions_at_most_three">¡Puedes seleccionar como máximo tres acciones para mostrar en la notificación compacta!</string>
|
||||
<string name="notification_actions_summary">Edite cada una de las acciones de notificación que aparecen a continuación pulsando sobre ellas. Seleccione hasta tres de ellas para que se muestren en la notificación compacta utilizando las casillas de verificación de la derecha</string>
|
||||
<string name="notification_actions_summary">Edite cada acción de notificación pulsando sobre ella. Seleccione hasta tres de ellas para que se muestren en la notificación compacta utilizando las casillas de verificación de la derecha.</string>
|
||||
<string name="notification_action_4_title">Botón de quinta acción</string>
|
||||
<string name="notification_action_3_title">Botón de cuarta acción</string>
|
||||
<string name="notification_action_2_title">Botón de tercera acción</string>
|
||||
|
@ -667,7 +667,7 @@
|
|||
<string name="enqueued_next">Añadido el siguiente vídeo a la cola</string>
|
||||
<string name="enqueue_next_stream">Añadir el siguiente vídeo a la cola</string>
|
||||
<string name="processing_may_take_a_moment">Procesando… Podría tomar un momento</string>
|
||||
<string name="manual_update_title">Buscar actualizaciones</string>
|
||||
<string name="check_for_updates">Buscar actualizaciones</string>
|
||||
<string name="manual_update_description">Buscar nuevas versiones manualmente</string>
|
||||
<string name="checking_updates_toast">Buscando actualizaciones…</string>
|
||||
<string name="feed_new_items">Nuevos elementos en el muro</string>
|
||||
|
@ -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">Cortos</string>
|
||||
<string name="channel_tab_shorts">Shorts</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>
|
||||
|
@ -822,4 +822,23 @@
|
|||
<string name="share_playlist">Compartir la lista de reproducción</string>
|
||||
<string name="share_playlist_with_titles_message">Compartir las listas de reproducción con los detalles como el nombre de la lista y los títulos de los vídeos o como una simple lista de una dirección URL con los vídeos</string>
|
||||
<string name="video_details_list_item">- %1$s: %2$s</string>
|
||||
<plurals name="replies">
|
||||
<item quantity="one">%s respuesta</item>
|
||||
<item quantity="many">%s respuestas</item>
|
||||
<item quantity="other">%s respuestas</item>
|
||||
</plurals>
|
||||
<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">Sí</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>
|
|
@ -411,7 +411,7 @@
|
|||
<string name="notification_action_shuffle">Aja segi</string>
|
||||
<string name="notification_action_repeat">Korda</string>
|
||||
<string name="notification_actions_at_most_three">Sa saad valida kuni kolm tegevust, mida kuvatakse lühiteavituses!</string>
|
||||
<string name="notification_actions_summary">Muuda iga teavituse tegevusi sellel toksates. Vali märkekastides paremal kuni kolm teavitust, mida kuvada lühiteates</string>
|
||||
<string name="notification_actions_summary">Muuda iga teavituse tegevusi sellel toksates. Vali märkekastides paremal kuni kolm teavitust, mida kuvada lühiteates.</string>
|
||||
<string name="notification_action_4_title">Viies tegevusnupp</string>
|
||||
<string name="notification_action_3_title">Neljas tegevusnupp</string>
|
||||
<string name="notification_action_2_title">Kolmas tegevusnupp</string>
|
||||
|
@ -505,7 +505,7 @@
|
|||
<string name="clear_download_history">Kustuta allalaadimiste ajalugu</string>
|
||||
<string name="error_download_resource_gone">Seda allalaadimist ei saa uuesti alustada</string>
|
||||
<string name="error_timeout">Ühendus aegus</string>
|
||||
<string name="error_insufficient_storage">Seadmes pole enam ruumi</string>
|
||||
<string name="error_insufficient_storage_left">Seadmes pole enam ruumi</string>
|
||||
<string name="download_already_pending">Sellise nimega allalaadimine on juba pooleli</string>
|
||||
<string name="overwrite_failed">faili asendamine ei õnnestu</string>
|
||||
<string name="feed_create_new_group_button_title">Uus</string>
|
||||
|
@ -653,7 +653,7 @@
|
|||
<string name="enqueue_next_stream">Lisa esitamiseks järgmisena</string>
|
||||
<string name="processing_may_take_a_moment">Töötlen andmeid… Võib kuluda mõni hetk</string>
|
||||
<string name="checking_updates_toast">Kontrollin uuendusi…</string>
|
||||
<string name="manual_update_title">Kontrolli uuendusi</string>
|
||||
<string name="check_for_updates">Kontrolli uuendusi</string>
|
||||
<string name="manual_update_description">Kontrolli uuendusi käsitsi</string>
|
||||
<string name="feed_new_items">Uued andmevoo kirjed</string>
|
||||
<string name="show_crash_the_player_title">Näita „Jooksuta meediamängija kokku“ nupukest</string>
|
||||
|
@ -806,4 +806,22 @@
|
|||
<string name="share_playlist">Jaga esitusloendit</string>
|
||||
<string name="share_playlist_with_titles_message">Jaga esitusloendit kas väga detailse teabega palade kohta või lihtsa url\'ide loendina</string>
|
||||
<string name="video_details_list_item">- %1$s: %2$s</string>
|
||||
<string name="show_more">Näita veel</string>
|
||||
<plurals name="replies">
|
||||
<item quantity="one">%s vastus</item>
|
||||
<item quantity="other">%s vastust</item>
|
||||
</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>
|
|
@ -396,7 +396,7 @@
|
|||
<string name="overwrite_failed">Ezin da fitxategia gainidatzi</string>
|
||||
<string name="download_already_pending">Badago izen bereko deskarga bat burutzeke</string>
|
||||
<string name="error_postprocessing_stopped">NewPipe itxi egin da fitxategian lanean zegoela</string>
|
||||
<string name="error_insufficient_storage">Ez dago lekurik gailuan</string>
|
||||
<string name="error_insufficient_storage_left">Ez dago lekurik gailuan</string>
|
||||
<string name="error_progress_lost">Progresioa galdu da, fitxategia ezabatu delako</string>
|
||||
<string name="confirm_prompt">Zure deskargen historiala garbitu nahi duzu ala deskargatutako fitxategi guztiak ezabatu\?</string>
|
||||
<string name="enable_queue_limit">Mugatu deskargen ilara</string>
|
||||
|
@ -657,7 +657,7 @@
|
|||
<string name="error_report_channel_description">Jakinarazpenak erroreen berri emateko</string>
|
||||
<string name="error_report_notification_title">NewPipe-k errore bat aurkitu du, sakatu berri emateko</string>
|
||||
<string name="error_report_notification_toast">Errore bat gertatu da, ikusi jakinarazpena</string>
|
||||
<string name="manual_update_title">Bilatu eguneraketak</string>
|
||||
<string name="check_for_updates">Bilatu eguneraketak</string>
|
||||
<string name="manual_update_description">Bilatu bertsio berriak eskuz</string>
|
||||
<string name="feed_new_items">Elementu berriak jarioan</string>
|
||||
<string name="no_appropriate_file_manager_message_android_10">Ez da fitxategi kudeatzaile bat aurkitu ekintza honetarako.
|
||||
|
|
|
@ -384,7 +384,7 @@
|
|||
<string name="overwrite_failed">ناتوانی در بازنویسی پرونده</string>
|
||||
<string name="download_already_pending">یک بارگیری دیگر با همین نام در صف قرار دارد</string>
|
||||
<string name="error_postprocessing_stopped">نیوپایپ در خلال کار روی پرونده، بسته شد</string>
|
||||
<string name="error_insufficient_storage">فضایی روی دستگاه باقی نمانده است</string>
|
||||
<string name="error_insufficient_storage_left">فضایی روی دستگاه باقی نمانده است</string>
|
||||
<string name="error_progress_lost">پیشرفت کار متوفق شد زیرا پرونده پاک شده است</string>
|
||||
<string name="error_timeout">پایان زمان اتصال</string>
|
||||
<string name="confirm_prompt">میخواهید تاریخچه بارگیری را پاک کنید یا همه پروندههایی که بارگیری شدهاند؟</string>
|
||||
|
@ -652,7 +652,7 @@
|
|||
<string name="enqueued_next">بعدی در صف گذاشته شد</string>
|
||||
<string name="enqueue_next_stream">در صف گذاشتن بعدی</string>
|
||||
<string name="processing_may_take_a_moment">در حال پردازش… ممکن است کمی طول بکشد</string>
|
||||
<string name="manual_update_title">بررسی بهروز رسانیها</string>
|
||||
<string name="check_for_updates">بررسی بهروز رسانیها</string>
|
||||
<string name="manual_update_description">بررسی دستی برای نگارشهای جدید</string>
|
||||
<string name="checking_updates_toast">بررسی کردن بهروز رسانیها…</string>
|
||||
<string name="feed_new_items">موارد خوراک جدید</string>
|
||||
|
|
|
@ -481,7 +481,7 @@
|
|||
<string name="error_download_resource_gone">Tätä latausta ei voi palauttaa</string>
|
||||
<string name="error_timeout">Yhteys aikakatkaistiin</string>
|
||||
<string name="error_progress_lost">Eteneminen menetettiin, koska tiedosto poistettiin</string>
|
||||
<string name="error_insufficient_storage">Laitteella ei ole tilaa</string>
|
||||
<string name="error_insufficient_storage_left">Laitteella ei ole tilaa</string>
|
||||
<string name="error_postprocessing_stopped">NewPipe suljettiin, kun se käsitteli tiedostoa</string>
|
||||
<string name="error_postprocessing_failed">Jälkikäsittely epäonnistui</string>
|
||||
<string name="error_http_not_found">Ei löytynyt</string>
|
||||
|
@ -652,7 +652,7 @@
|
|||
<item quantity="other">Poistettu %1$s latausta</item>
|
||||
</plurals>
|
||||
<string name="processing_may_take_a_moment">Käsitellään… Voi kestää hetken</string>
|
||||
<string name="manual_update_title">Tarkista päivitykset</string>
|
||||
<string name="check_for_updates">Tarkista päivitykset</string>
|
||||
<string name="manual_update_description">Tarkista manuaalisesti onko uusia versioita saatavilla</string>
|
||||
<string name="checking_updates_toast">Tarkistetaan päivityksiä…</string>
|
||||
<string name="error_report_channel_description">Ilmoitukset, joilla raportoidaan virheistä</string>
|
||||
|
@ -789,7 +789,14 @@
|
|||
<string name="channel_tab_channels">Kanavat</string>
|
||||
<string name="you_successfully_subscribed">Olet nyt tilannut tämän kanavan</string>
|
||||
<string name="previous_stream">Edellinen stream</string>
|
||||
<string name="channel_tab_livestreams"/>
|
||||
<string name="channel_tab_livestreams">Live</string>
|
||||
<string name="remove_duplicates_message">Haluatko poistaa kaikki ylimääräiset identtiset suoratoistot tästä soittolistasta\?</string>
|
||||
<string name="streams_not_yet_supported_removed">Suoratoistot, joita lataaja ei vielä tue, ei näytetä</string>
|
||||
<plurals name="replies">
|
||||
<item quantity="one">%s vastaus</item>
|
||||
<item quantity="other">%s vastausta</item>
|
||||
</plurals>
|
||||
<string name="show_more">Näytä lisää</string>
|
||||
<string name="show_less">Näytä vähemmän</string>
|
||||
<string name="show_channel_tabs">Kanavan välilehdet</string>
|
||||
</resources>
|
|
@ -395,7 +395,7 @@
|
|||
<string name="overwrite_failed">impossible d’écraser le fichier</string>
|
||||
<string name="download_already_pending">Il y a un téléchargement en attente avec ce nom</string>
|
||||
<string name="error_postprocessing_stopped">NewPipe a été fermé alors qu’il travaillait sur le fichier</string>
|
||||
<string name="error_insufficient_storage">Aucun espace disponible sur l’appareil</string>
|
||||
<string name="error_insufficient_storage_left">Aucun espace disponible sur l’appareil</string>
|
||||
<string name="error_progress_lost">Progression perdue car le fichier a été supprimé</string>
|
||||
<string name="confirm_prompt">Voulez-vous effacer l’historique de téléchargement ou supprimer tous les fichiers téléchargés \?</string>
|
||||
<string name="enable_queue_limit">Limiter la file d’attente de téléchargement</string>
|
||||
|
@ -557,7 +557,7 @@
|
|||
<string name="notification_action_shuffle">Lire aléatoirement</string>
|
||||
<string name="notification_action_repeat">Répéter</string>
|
||||
<string name="notification_actions_at_most_three">Vous pouvez sélectionner au maximum trois actions à faire figurer dans la notification compacte !</string>
|
||||
<string name="notification_actions_summary">Modifiez chaque action de notification ci-dessous en appuyant dessus. Sélectionnez jusqu’à trois d’entre elles pour les faire apparaitre dans la notification compacte en utilisant les cases à cocher à droite</string>
|
||||
<string name="notification_actions_summary">Modifiez chaque action de notification ci-dessous en appuyant dessus. Sélectionnez jusqu’à trois d’entre elles pour les faire apparaître dans la notification compacte en utilisant les cases à cocher à droite.</string>
|
||||
<string name="notification_action_4_title">Cinquième bouton d’action</string>
|
||||
<string name="notification_action_3_title">Quatrième bouton d’action</string>
|
||||
<string name="notification_action_2_title">Troisième bouton d’action</string>
|
||||
|
@ -668,7 +668,7 @@
|
|||
<string name="processing_may_take_a_moment">Traitement en cours… Veuillez patienter</string>
|
||||
<string name="manual_update_description">Vérifier manuellement de nouvelles versions</string>
|
||||
<string name="checking_updates_toast">Vérification des mises à jour…</string>
|
||||
<string name="manual_update_title">Vérifier les mises à jour</string>
|
||||
<string name="check_for_updates">Vérifier les mises à jour</string>
|
||||
<string name="feed_new_items">Nouveaux éléments du flux</string>
|
||||
<string name="crash_the_player">Faire planter le lecteur</string>
|
||||
<string name="show_crash_the_player_title">Afficher « Faire planter le lecteur »</string>
|
||||
|
@ -821,4 +821,23 @@
|
|||
<string name="metadata_uploader_avatars">Avatars du téléverseur</string>
|
||||
<string name="image_quality_summary">Sélectionnez la qualité des images et si les images doivent être chargées, pour réduire l\'utilisation de la mémoire et de données. Les modifications vident à la fois le cache des images en mémoire et sur le disque — %s</string>
|
||||
<string name="play">Lire</string>
|
||||
<plurals name="replies">
|
||||
<item quantity="one">%s réponse</item>
|
||||
<item quantity="many">%s réponses</item>
|
||||
<item quantity="other">%s réponses</item>
|
||||
</plurals>
|
||||
<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>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue