mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2025-04-23 03:03:18 +00:00
Compare commits
125 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
276bf390b2 | ||
![]() |
f39eda086f | ||
![]() |
756327da39 | ||
![]() |
5840d3a437 | ||
![]() |
e1dedd45ed | ||
![]() |
912f07a1dd | ||
![]() |
205466c56a | ||
![]() |
7f10312d0a | ||
![]() |
63be3220e7 | ||
![]() |
536b78f2e6 | ||
![]() |
6d6b73ef73 | ||
![]() |
196c27792b | ||
![]() |
b3789315ad | ||
![]() |
c7bf498c04 | ||
![]() |
35abb99dac | ||
![]() |
70416e73f3 | ||
![]() |
a0b76c3385 | ||
![]() |
c232193a46 | ||
![]() |
f289bea6b3 | ||
![]() |
48b200868a | ||
![]() |
54bf7f0ced | ||
![]() |
980a35a708 | ||
![]() |
da106e2361 | ||
![]() |
3532ac96b4 | ||
![]() |
87693a2ad1 | ||
![]() |
d321e57620 | ||
![]() |
fb4a65a14a | ||
![]() |
3047704e1c | ||
![]() |
3dcfdaf510 | ||
![]() |
2ceb70236e | ||
![]() |
be097f26c8 | ||
![]() |
098f60d593 | ||
![]() |
eb0568044a | ||
![]() |
f3b3d5c3e7 | ||
![]() |
b888dc72cf | ||
![]() |
599d86151a | ||
![]() |
587df093ea | ||
![]() |
8830e87242 | ||
![]() |
f96b8f7b2a | ||
![]() |
c28478ae53 | ||
![]() |
10110397fd | ||
![]() |
d81244e77c | ||
![]() |
ea20ca9e72 | ||
![]() |
f0c89494dd | ||
![]() |
0fd2d4fed6 | ||
![]() |
3c7b026d7d | ||
![]() |
998d84de6c | ||
![]() |
76a02d5858 | ||
![]() |
24bb71a23f | ||
![]() |
49b71942ad | ||
![]() |
c9ec257a5e | ||
![]() |
b1f995a78c | ||
![]() |
acac50a1d1 | ||
![]() |
c6b87cd316 | ||
![]() |
94d4c21cc7 | ||
![]() |
a7a7dc5363 | ||
![]() |
126f4b0e30 | ||
![]() |
6558794d26 | ||
![]() |
1d12874937 | ||
![]() |
1d98518bfa | ||
![]() |
e5458bcb14 | ||
![]() |
dc62d211f5 | ||
![]() |
ec6612dd71 | ||
![]() |
064e1d39c7 | ||
![]() |
4c88a193bd | ||
![]() |
3fcac10e7f | ||
![]() |
6cedd117fe | ||
![]() |
5eabcb52b5 | ||
![]() |
690b40d0c4 | ||
![]() |
9bb2c0b484 | ||
![]() |
1e08cc8c8f | ||
![]() |
7d17468266 | ||
![]() |
5819546ea9 | ||
![]() |
cfb6e114d6 | ||
![]() |
b764ad33c4 | ||
![]() |
430b4eb916 | ||
![]() |
2339f51ad4 | ||
![]() |
c6e1721884 | ||
![]() |
94684fe380 | ||
![]() |
398a2f55ce | ||
![]() |
1f7b3b5b06 | ||
![]() |
909ed616c4 | ||
![]() |
dd223af28d | ||
![]() |
dbee8d8128 | ||
![]() |
b62a09b5b3 | ||
![]() |
87317c6faf | ||
![]() |
53b599b042 | ||
![]() |
21df24abfd | ||
![]() |
ca4592a935 | ||
![]() |
3fc487310b | ||
![]() |
056809cb0d | ||
![]() |
a60bb3e7af | ||
![]() |
ecd3f6c2ee | ||
![]() |
70ff47b810 | ||
![]() |
b8e050f6c4 | ||
![]() |
46d0bc1004 | ||
![]() |
e7fe84f2c7 | ||
![]() |
2b183a0576 | ||
![]() |
f856bd9306 | ||
![]() |
0066b322e1 | ||
![]() |
3bdae81c0a | ||
![]() |
6010c4ea7f | ||
![]() |
690b3410e9 | ||
![]() |
ba86ce137b | ||
![]() |
410c01547c | ||
![]() |
47263f5254 | ||
![]() |
01bf855015 | ||
![]() |
ebf3008729 | ||
![]() |
33ecfb757e | ||
![]() |
ffe26d882b | ||
![]() |
83f8141fe7 | ||
![]() |
9253640fae | ||
![]() |
8b5aa5cd9b | ||
![]() |
a955408053 | ||
![]() |
86203d6800 | ||
![]() |
edd19641ac | ||
![]() |
65749cbac0 | ||
![]() |
658ddfc921 | ||
![]() |
f7d0fd545d | ||
![]() |
27e6be792f | ||
![]() |
3fc0147f47 | ||
![]() |
c6b05c6094 | ||
![]() |
240a2fe36b | ||
![]() |
de46e3abb3 | ||
![]() |
70748fa0bc |
2
.github/CONTRIBUTING.md
vendored
2
.github/CONTRIBUTING.md
vendored
@ -6,7 +6,7 @@ NewPipe contribution guidelines
|
||||
## Crash reporting
|
||||
|
||||
Report crashes through the **automated crash report system** of NewPipe.
|
||||
This way all the data needed for debugging is included in your bugreport for GitHub.
|
||||
This way all the data needed for debugging is included in your bug report for GitHub.
|
||||
You'll see *exactly* what is sent, be able to add **your comments**, and then send it.
|
||||
|
||||
## Issue reporting/feature requests
|
||||
|
2
.github/workflows/build-release-apk.yml
vendored
2
.github/workflows/build-release-apk.yml
vendored
@ -14,7 +14,7 @@ jobs:
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
java-version: '21'
|
||||
cache: 'gradle'
|
||||
|
||||
- name: "Build release APK"
|
||||
|
4
.github/workflows/image-minimizer.js
vendored
4
.github/workflows/image-minimizer.js
vendored
@ -32,8 +32,8 @@ module.exports = async ({github, context}) => {
|
||||
}
|
||||
|
||||
// Regex for finding images (simple variant) 
|
||||
const REGEX_USER_CONTENT_IMAGE_LOOKUP = /\!\[(.*)\]\((https:\/\/[-a-z0-9]+\.githubusercontent\.com\/\d+\/[-0-9a-f]{32,512}\.(jpg|gif|png))\)/gm;
|
||||
const REGEX_ASSETS_IMAGE_LOCKUP = /\!\[(.*)\]\((https:\/\/github\.com\/[-\w\d]+\/[-\w\d]+\/assets\/\d+\/[\-0-9a-f]{32,512})\)/gm;
|
||||
const REGEX_USER_CONTENT_IMAGE_LOOKUP = /\!\[([^\]]*)\]\((https:\/\/[-a-z0-9]+\.githubusercontent\.com\/\d+\/[-0-9a-f]{32,512}\.(jpg|gif|png))\)/gm;
|
||||
const REGEX_ASSETS_IMAGE_LOCKUP = /\!\[([^\]]*)\]\((https:\/\/github\.com\/[-\w\d]+\/[-\w\d]+\/assets\/\d+\/[\-0-9a-f]{32,512})\)/gm;
|
||||
|
||||
// Check if we found something
|
||||
let foundSimpleImages = REGEX_USER_CONTENT_IMAGE_LOOKUP.test(initialBody)
|
||||
|
@ -23,9 +23,9 @@ android {
|
||||
if (System.properties.containsKey('versionCodeOverride')) {
|
||||
versionCode System.getProperty('versionCodeOverride') as Integer
|
||||
} else {
|
||||
versionCode 1002
|
||||
versionCode 1003
|
||||
}
|
||||
versionName "0.27.5"
|
||||
versionName "0.27.6"
|
||||
if (System.properties.containsKey('versionNameSuffix')) {
|
||||
versionNameSuffix System.getProperty('versionNameSuffix')
|
||||
}
|
||||
@ -97,6 +97,10 @@ android {
|
||||
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
|
||||
}
|
||||
|
||||
androidResources {
|
||||
generateLocaleConfig = true
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
viewBinding true
|
||||
buildConfig true
|
||||
@ -208,7 +212,7 @@ dependencies {
|
||||
implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751'
|
||||
// WORKAROUND: if you get errors with the NewPipeExtractor dependency, replace `v0.24.3` with
|
||||
// the corresponding commit hash, since JitPack is sometimes buggy
|
||||
implementation 'com.github.TeamNewPipe:NewPipeExtractor:v0.24.4'
|
||||
implementation 'com.github.TeamNewPipe:NewPipeExtractor:0b99100db'
|
||||
implementation 'com.github.TeamNewPipe:NoNonsense-FilePicker:5.0.0'
|
||||
|
||||
/** Checkstyle **/
|
||||
@ -241,6 +245,7 @@ dependencies {
|
||||
implementation "androidx.work:work-runtime-ktx:${androidxWorkVersion}"
|
||||
implementation "androidx.work:work-rxjava3:${androidxWorkVersion}"
|
||||
implementation 'com.google.android.material:material:1.11.0'
|
||||
implementation "androidx.webkit:webkit:1.9.0"
|
||||
|
||||
/** Third-party libraries **/
|
||||
// Instance state boilerplate elimination
|
||||
|
7
app/proguard-rules.pro
vendored
7
app/proguard-rules.pro
vendored
@ -5,10 +5,17 @@
|
||||
|
||||
## Rules for NewPipeExtractor
|
||||
-keep class org.schabi.newpipe.extractor.timeago.patterns.** { *; }
|
||||
## Rules for Rhino and Rhino Engine
|
||||
-keep class org.mozilla.javascript.* { *; }
|
||||
-keep class org.mozilla.javascript.** { *; }
|
||||
-keep class org.mozilla.javascript.engine.** { *; }
|
||||
-keep class org.mozilla.classfile.ClassFileWriter
|
||||
-dontwarn org.mozilla.javascript.JavaToJSONConverters
|
||||
-dontwarn org.mozilla.javascript.tools.**
|
||||
-keep class javax.script.** { *; }
|
||||
-dontwarn javax.script.**
|
||||
-keep class jdk.dynalink.** { *; }
|
||||
-dontwarn jdk.dynalink.**
|
||||
|
||||
## Rules for ExoPlayer
|
||||
-keep class com.google.android.exoplayer2.** { *; }
|
||||
|
@ -64,6 +64,9 @@
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.media.browse.MediaBrowserService"/>
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<activity
|
||||
@ -424,5 +427,10 @@
|
||||
<meta-data
|
||||
android:name="com.samsung.android.multidisplay.keep_process_alive"
|
||||
android:value="true" />
|
||||
<!-- Android Auto -->
|
||||
<meta-data android:name="com.google.android.gms.car.application"
|
||||
android:resource="@xml/automotive_app_desc" />
|
||||
<meta-data android:name="com.google.android.gms.car.notification.SmallIcon"
|
||||
android:resource="@mipmap/ic_launcher" />
|
||||
</application>
|
||||
</manifest>
|
||||
|
127
app/src/main/assets/po_token.html
Normal file
127
app/src/main/assets/po_token.html
Normal file
@ -0,0 +1,127 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en"><head><title></title><script>
|
||||
/**
|
||||
* Factory method to create and load a BotGuardClient instance.
|
||||
* @param options - Configuration options for the BotGuardClient.
|
||||
* @returns A promise that resolves to a loaded BotGuardClient instance.
|
||||
*/
|
||||
function loadBotGuard(challengeData) {
|
||||
this.vm = this[challengeData.globalName];
|
||||
this.program = challengeData.program;
|
||||
this.vmFunctions = {};
|
||||
this.syncSnapshotFunction = null;
|
||||
|
||||
if (!this.vm)
|
||||
throw new Error('[BotGuardClient]: VM not found in the global object');
|
||||
|
||||
if (!this.vm.a)
|
||||
throw new Error('[BotGuardClient]: Could not load program');
|
||||
|
||||
const vmFunctionsCallback = function (
|
||||
asyncSnapshotFunction,
|
||||
shutdownFunction,
|
||||
passEventFunction,
|
||||
checkCameraFunction
|
||||
) {
|
||||
this.vmFunctions = {
|
||||
asyncSnapshotFunction: asyncSnapshotFunction,
|
||||
shutdownFunction: shutdownFunction,
|
||||
passEventFunction: passEventFunction,
|
||||
checkCameraFunction: checkCameraFunction
|
||||
};
|
||||
};
|
||||
|
||||
this.syncSnapshotFunction = this.vm.a(this.program, vmFunctionsCallback, true, this.userInteractionElement, function () {/** no-op */ }, [ [], [] ])[0]
|
||||
|
||||
// an asynchronous function runs in the background and it will eventually call
|
||||
// `vmFunctionsCallback`, however we need to manually tell JavaScript to pass
|
||||
// control to the things running in the background by interrupting this async
|
||||
// function in any way, e.g. with a delay of 1ms. The loop is most probably not
|
||||
// needed but is there just because.
|
||||
return new Promise(function (resolve, reject) {
|
||||
i = 0
|
||||
refreshIntervalId = setInterval(function () {
|
||||
if (!!this.vmFunctions.asyncSnapshotFunction) {
|
||||
resolve(this)
|
||||
clearInterval(refreshIntervalId);
|
||||
}
|
||||
if (i >= 10000) {
|
||||
reject("asyncSnapshotFunction is null even after 10 seconds")
|
||||
clearInterval(refreshIntervalId);
|
||||
}
|
||||
i += 1;
|
||||
}, 1);
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a snapshot asynchronously.
|
||||
* @returns The snapshot result.
|
||||
* @example
|
||||
* ```ts
|
||||
* const result = await botguard.snapshot({
|
||||
* contentBinding: {
|
||||
* c: "a=6&a2=10&b=SZWDwKVIuixOp7Y4euGTgwckbJA&c=1729143849&d=1&t=7200&c1a=1&c6a=1&c6b=1&hh=HrMb5mRWTyxGJphDr0nW2Oxonh0_wl2BDqWuLHyeKLo",
|
||||
* e: "ENGAGEMENT_TYPE_VIDEO_LIKE",
|
||||
* encryptedVideoId: "P-vC09ZJcnM"
|
||||
* }
|
||||
* });
|
||||
*
|
||||
* console.log(result);
|
||||
* ```
|
||||
*/
|
||||
function snapshot(args) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
if (!this.vmFunctions.asyncSnapshotFunction)
|
||||
return reject(new Error('[BotGuardClient]: Async snapshot function not found'));
|
||||
|
||||
this.vmFunctions.asyncSnapshotFunction(function (response) { resolve(response) }, [
|
||||
args.contentBinding,
|
||||
args.signedTimestamp,
|
||||
args.webPoSignalOutput,
|
||||
args.skipPrivacyBuffer
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
function runBotGuard(challengeData) {
|
||||
const interpreterJavascript = challengeData.interpreterJavascript.privateDoNotAccessOrElseSafeScriptWrappedValue;
|
||||
|
||||
if (interpreterJavascript) {
|
||||
new Function(interpreterJavascript)();
|
||||
} else throw new Error('Could not load VM');
|
||||
|
||||
const webPoSignalOutput = [];
|
||||
return loadBotGuard({
|
||||
globalName: challengeData.globalName,
|
||||
globalObj: this,
|
||||
program: challengeData.program
|
||||
}).then(function (botguard) {
|
||||
return botguard.snapshot({ webPoSignalOutput: webPoSignalOutput })
|
||||
}).then(function (botguardResponse) {
|
||||
return { webPoSignalOutput: webPoSignalOutput, botguardResponse: botguardResponse }
|
||||
})
|
||||
}
|
||||
|
||||
function obtainPoToken(webPoSignalOutput, integrityToken, identifier) {
|
||||
const getMinter = webPoSignalOutput[0];
|
||||
|
||||
if (!getMinter)
|
||||
throw new Error('PMD:Undefined');
|
||||
|
||||
const mintCallback = getMinter(integrityToken);
|
||||
|
||||
if (!(mintCallback instanceof Function))
|
||||
throw new Error('APF:Failed');
|
||||
|
||||
const result = mintCallback(identifier);
|
||||
|
||||
if (!result)
|
||||
throw new Error('YNJ:Undefined');
|
||||
|
||||
if (!(result instanceof Uint8Array))
|
||||
throw new Error('ODM:Invalid');
|
||||
|
||||
return result;
|
||||
}
|
||||
</script></head><body></body></html>
|
@ -17,6 +17,7 @@ import org.acra.config.CoreConfigurationBuilder;
|
||||
import org.schabi.newpipe.error.ReCaptchaActivity;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.downloader.Downloader;
|
||||
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor;
|
||||
import org.schabi.newpipe.ktx.ExceptionUtils;
|
||||
import org.schabi.newpipe.settings.NewPipeSettings;
|
||||
import org.schabi.newpipe.util.BridgeStateSaverInitializer;
|
||||
@ -26,6 +27,7 @@ import org.schabi.newpipe.util.StateSaver;
|
||||
import org.schabi.newpipe.util.image.ImageStrategy;
|
||||
import org.schabi.newpipe.util.image.PicassoHelper;
|
||||
import org.schabi.newpipe.util.image.PreferredImageQuality;
|
||||
import org.schabi.newpipe.util.potoken.PoTokenProviderImpl;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InterruptedIOException;
|
||||
@ -118,6 +120,8 @@ public class App extends Application {
|
||||
&& prefs.getBoolean(getString(R.string.show_image_indicators_key), false));
|
||||
|
||||
configureRxJavaErrorHandler();
|
||||
|
||||
YoutubeStreamExtractor.setPoTokenProvider(PoTokenProviderImpl.INSTANCE);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -137,7 +137,8 @@ public final class DownloaderImpl extends Downloader {
|
||||
}
|
||||
|
||||
final okhttp3.Request.Builder requestBuilder = new okhttp3.Request.Builder()
|
||||
.method(httpMethod, requestBody).url(url)
|
||||
.method(httpMethod, requestBody)
|
||||
.url(url)
|
||||
.addHeader("User-Agent", USER_AGENT);
|
||||
|
||||
final String cookies = getCookies(url);
|
||||
@ -145,38 +146,33 @@ public final class DownloaderImpl extends Downloader {
|
||||
requestBuilder.addHeader("Cookie", cookies);
|
||||
}
|
||||
|
||||
for (final Map.Entry<String, List<String>> pair : headers.entrySet()) {
|
||||
final String headerName = pair.getKey();
|
||||
final List<String> headerValueList = pair.getValue();
|
||||
headers.forEach((headerName, headerValueList) -> {
|
||||
requestBuilder.removeHeader(headerName);
|
||||
headerValueList.forEach(headerValue ->
|
||||
requestBuilder.addHeader(headerName, headerValue));
|
||||
});
|
||||
|
||||
if (headerValueList.size() > 1) {
|
||||
requestBuilder.removeHeader(headerName);
|
||||
for (final String headerValue : headerValueList) {
|
||||
requestBuilder.addHeader(headerName, headerValue);
|
||||
}
|
||||
} else if (headerValueList.size() == 1) {
|
||||
requestBuilder.header(headerName, headerValueList.get(0));
|
||||
try (
|
||||
okhttp3.Response response = client.newCall(requestBuilder.build()).execute()
|
||||
) {
|
||||
if (response.code() == 429) {
|
||||
throw new ReCaptchaException("reCaptcha Challenge requested", url);
|
||||
}
|
||||
|
||||
String responseBodyToReturn = null;
|
||||
try (ResponseBody body = response.body()) {
|
||||
if (body != null) {
|
||||
responseBodyToReturn = body.string();
|
||||
}
|
||||
}
|
||||
|
||||
final String latestUrl = response.request().url().toString();
|
||||
return new Response(
|
||||
response.code(),
|
||||
response.message(),
|
||||
response.headers().toMultimap(),
|
||||
responseBodyToReturn,
|
||||
latestUrl);
|
||||
}
|
||||
|
||||
final okhttp3.Response response = client.newCall(requestBuilder.build()).execute();
|
||||
|
||||
if (response.code() == 429) {
|
||||
response.close();
|
||||
|
||||
throw new ReCaptchaException("reCaptcha Challenge requested", url);
|
||||
}
|
||||
|
||||
final ResponseBody body = response.body();
|
||||
String responseBodyToReturn = null;
|
||||
|
||||
if (body != null) {
|
||||
responseBodyToReturn = body.string();
|
||||
}
|
||||
|
||||
final String latestUrl = response.request().url().toString();
|
||||
return new Response(response.code(), response.message(), response.headers().toMultimap(),
|
||||
responseBodyToReturn, latestUrl);
|
||||
}
|
||||
}
|
||||
|
@ -38,6 +38,7 @@ import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.webkit.WebView;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.FrameLayout;
|
||||
@ -92,6 +93,7 @@ import org.schabi.newpipe.util.SerializedCache;
|
||||
import org.schabi.newpipe.util.ServiceHelper;
|
||||
import org.schabi.newpipe.util.StateSaver;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||
import org.schabi.newpipe.views.FocusOverlayView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@ -120,7 +122,8 @@ public class MainActivity extends AppCompatActivity {
|
||||
private static final int ITEM_ID_DOWNLOADS = -4;
|
||||
private static final int ITEM_ID_HISTORY = -5;
|
||||
private static final int ITEM_ID_SETTINGS = 0;
|
||||
private static final int ITEM_ID_ABOUT = 1;
|
||||
private static final int ITEM_ID_DONATION = 1;
|
||||
private static final int ITEM_ID_ABOUT = 2;
|
||||
|
||||
private static final int ORDER = 0;
|
||||
|
||||
@ -138,6 +141,19 @@ public class MainActivity extends AppCompatActivity {
|
||||
ThemeHelper.setDayNightMode(this);
|
||||
ThemeHelper.setTheme(this, ServiceHelper.getSelectedServiceId(this));
|
||||
|
||||
// Fixes text color turning black in dark/black mode:
|
||||
// https://github.com/TeamNewPipe/NewPipe/issues/12016
|
||||
// For further reference see: https://issuetracker.google.com/issues/37124582
|
||||
if (DeviceUtils.supportsWebView()) {
|
||||
try {
|
||||
new WebView(this);
|
||||
} catch (final Throwable e) {
|
||||
if (DEBUG) {
|
||||
Log.e(TAG, "Failed to create WebView", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assureCorrectAppLanguage(this);
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
@ -174,6 +190,8 @@ public class MainActivity extends AppCompatActivity {
|
||||
&& ReleaseVersionUtil.INSTANCE.isReleaseApk()) {
|
||||
UpdateSettingsFragment.askForConsentToUpdateChecks(this);
|
||||
}
|
||||
|
||||
Localization.migrateAppLanguageSettingIfNecessary(getApplicationContext());
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -262,6 +280,10 @@ public class MainActivity extends AppCompatActivity {
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_options_about_group, ITEM_ID_SETTINGS, ORDER, R.string.settings)
|
||||
.setIcon(R.drawable.ic_settings);
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_options_about_group, ITEM_ID_DONATION, ORDER,
|
||||
R.string.donation_title)
|
||||
.setIcon(R.drawable.volunteer_activism_ic);
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_options_about_group, ITEM_ID_ABOUT, ORDER, R.string.tab_about)
|
||||
.setIcon(R.drawable.ic_info_outline);
|
||||
@ -337,6 +359,9 @@ public class MainActivity extends AppCompatActivity {
|
||||
case ITEM_ID_SETTINGS:
|
||||
NavigationHelper.openSettings(this);
|
||||
break;
|
||||
case ITEM_ID_DONATION:
|
||||
ShareUtils.openUrlInBrowser(this, getString(R.string.donation_url));
|
||||
break;
|
||||
case ITEM_ID_ABOUT:
|
||||
NavigationHelper.openAbout(this);
|
||||
break;
|
||||
@ -839,7 +864,8 @@ public class MainActivity extends AppCompatActivity {
|
||||
@Override
|
||||
public void onReceive(final Context context, final Intent intent) {
|
||||
if (Objects.equals(intent.getAction(),
|
||||
VideoDetailFragment.ACTION_PLAYER_STARTED)) {
|
||||
VideoDetailFragment.ACTION_PLAYER_STARTED)
|
||||
&& PlayerHolder.getInstance().isPlayerOpen()) {
|
||||
openMiniPlayerIfMissing();
|
||||
// At this point the player is added 100%, we can unregister. Other actions
|
||||
// are useless since the fragment will not be removed after that.
|
||||
@ -851,6 +877,10 @@ public class MainActivity extends AppCompatActivity {
|
||||
final IntentFilter intentFilter = new IntentFilter();
|
||||
intentFilter.addAction(VideoDetailFragment.ACTION_PLAYER_STARTED);
|
||||
registerReceiver(broadcastReceiver, intentFilter);
|
||||
|
||||
// If the PlayerHolder is not bound yet, but the service is running, try to bind to it.
|
||||
// Once the connection is established, the ACTION_PLAYER_STARTED will be sent.
|
||||
PlayerHolder.getInstance().tryBindIfNeeded(this);
|
||||
}
|
||||
}
|
||||
|
||||
@ -924,4 +954,5 @@ public class MainActivity extends AppCompatActivity {
|
||||
return sheetState == BottomSheetBehavior.STATE_HIDDEN
|
||||
|| sheetState == BottomSheetBehavior.STATE_COLLAPSED;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -3,6 +3,8 @@ package org.schabi.newpipe.database.history.model
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Embedded
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||
import org.schabi.newpipe.util.image.ImageStrategy
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
data class StreamHistoryEntry(
|
||||
@ -27,4 +29,17 @@ data class StreamHistoryEntry(
|
||||
return this.streamEntity.uid == other.streamEntity.uid && streamId == other.streamId &&
|
||||
accessDate.isEqual(other.accessDate)
|
||||
}
|
||||
|
||||
fun toStreamInfoItem(): StreamInfoItem =
|
||||
StreamInfoItem(
|
||||
streamEntity.serviceId,
|
||||
streamEntity.url,
|
||||
streamEntity.title,
|
||||
streamEntity.streamType,
|
||||
).apply {
|
||||
duration = streamEntity.duration
|
||||
uploaderName = streamEntity.uploader
|
||||
uploaderUrl = streamEntity.uploaderUrl
|
||||
thumbnails = ImageStrategy.dbUrlToImageList(streamEntity.thumbnailUrl)
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
package org.schabi.newpipe.database.playlist;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.schabi.newpipe.database.LocalItem;
|
||||
|
||||
public interface PlaylistLocalItem extends LocalItem {
|
||||
@ -10,4 +12,7 @@ public interface PlaylistLocalItem extends LocalItem {
|
||||
long getUid();
|
||||
|
||||
void setDisplayIndex(long displayIndex);
|
||||
|
||||
@Nullable
|
||||
String getThumbnailUrl();
|
||||
}
|
||||
|
@ -9,6 +9,8 @@ import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST
|
||||
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 androidx.annotation.Nullable;
|
||||
|
||||
public class PlaylistMetadataEntry implements PlaylistLocalItem {
|
||||
public static final String PLAYLIST_STREAM_COUNT = "streamCount";
|
||||
|
||||
@ -71,4 +73,10 @@ public class PlaylistMetadataEntry implements PlaylistLocalItem {
|
||||
public void setDisplayIndex(final long displayIndex) {
|
||||
this.displayIndex = displayIndex;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public String getThumbnailUrl() {
|
||||
return thumbnailUrl;
|
||||
}
|
||||
}
|
||||
|
@ -34,7 +34,7 @@ public interface PlaylistRemoteDAO extends BasicDAO<PlaylistRemoteEntity> {
|
||||
|
||||
@Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE + " WHERE "
|
||||
+ REMOTE_PLAYLIST_ID + " = :playlistId")
|
||||
Flowable<List<PlaylistRemoteEntity>> getPlaylist(long playlistId);
|
||||
Flowable<PlaylistRemoteEntity> getPlaylist(long playlistId);
|
||||
|
||||
@Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE + " WHERE "
|
||||
+ REMOTE_PLAYLIST_URL + " = :url AND " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId")
|
||||
|
@ -2,6 +2,7 @@ package org.schabi.newpipe.database.playlist.model;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.room.ColumnInfo;
|
||||
import androidx.room.Entity;
|
||||
import androidx.room.Ignore;
|
||||
@ -134,6 +135,8 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public String getThumbnailUrl() {
|
||||
return thumbnailUrl;
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Arrays;
|
||||
import java.util.stream.Collectors;
|
||||
@ -67,10 +67,6 @@ public class ErrorActivity extends AppCompatActivity {
|
||||
public static final String ERROR_GITHUB_ISSUE_URL =
|
||||
"https://github.com/TeamNewPipe/NewPipe/issues";
|
||||
|
||||
public static final DateTimeFormatter CURRENT_TIMESTAMP_FORMATTER =
|
||||
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
|
||||
|
||||
|
||||
private ErrorInfo errorInfo;
|
||||
private String currentTimeStamp;
|
||||
|
||||
@ -107,7 +103,9 @@ public class ErrorActivity extends AppCompatActivity {
|
||||
|
||||
// important add guru meditation
|
||||
addGuruMeditation();
|
||||
currentTimeStamp = CURRENT_TIMESTAMP_FORMATTER.format(LocalDateTime.now());
|
||||
// print current time, as zoned ISO8601 timestamp
|
||||
final ZonedDateTime now = ZonedDateTime.now();
|
||||
currentTimeStamp = now.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME);
|
||||
|
||||
activityErrorBinding.errorReportEmailButton.setOnClickListener(v ->
|
||||
openPrivacyPolicyDialog(this, "EMAIL"));
|
||||
@ -250,6 +248,9 @@ public class ErrorActivity extends AppCompatActivity {
|
||||
.append("\n* __Content Language:__ ").append(getContentLanguageString())
|
||||
.append("\n* __App Language:__ ").append(getAppLanguage())
|
||||
.append("\n* __Service:__ ").append(errorInfo.getServiceName())
|
||||
.append("\n* __Timestamp:__ ").append(currentTimeStamp)
|
||||
.append("\n* __Package:__ ").append(getPackageName())
|
||||
.append("\n* __Service:__ ").append(errorInfo.getServiceName())
|
||||
.append("\n* __Version:__ ").append(BuildConfig.VERSION_NAME)
|
||||
.append("\n* __OS:__ ").append(getOsString()).append("\n");
|
||||
|
||||
|
@ -236,11 +236,14 @@ public final class VideoDetailFragment
|
||||
// Service management
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
@Override
|
||||
public void onServiceConnected(final Player connectedPlayer,
|
||||
final PlayerService connectedPlayerService,
|
||||
final boolean playAfterConnect) {
|
||||
player = connectedPlayer;
|
||||
public void onServiceConnected(@NonNull final PlayerService connectedPlayerService) {
|
||||
playerService = connectedPlayerService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayerConnected(@NonNull final Player connectedPlayer,
|
||||
final boolean playAfterConnect) {
|
||||
player = connectedPlayer;
|
||||
|
||||
// It will do nothing if the player is not in fullscreen mode
|
||||
hideSystemUiIfNeeded();
|
||||
@ -272,22 +275,29 @@ public final class VideoDetailFragment
|
||||
updateOverlayPlayQueueButtonVisibility();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayerDisconnected() {
|
||||
player = null;
|
||||
// the binding could be null at this point, if the app is finishing
|
||||
if (binding != null) {
|
||||
restoreDefaultBrightness();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected() {
|
||||
playerService = null;
|
||||
player = null;
|
||||
restoreDefaultBrightness();
|
||||
}
|
||||
|
||||
|
||||
/*////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public static VideoDetailFragment getInstance(final int serviceId,
|
||||
@Nullable final String videoUrl,
|
||||
@Nullable final String url,
|
||||
@NonNull final String name,
|
||||
@Nullable final PlayQueue queue) {
|
||||
final VideoDetailFragment instance = new VideoDetailFragment();
|
||||
instance.setInitialData(serviceId, videoUrl, name, queue);
|
||||
instance.setInitialData(serviceId, url, name, queue);
|
||||
return instance;
|
||||
}
|
||||
|
||||
@ -1736,7 +1746,7 @@ public final class VideoDetailFragment
|
||||
playQueue = queue;
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onQueueUpdate() called with: serviceId = ["
|
||||
+ serviceId + "], videoUrl = [" + url + "], name = ["
|
||||
+ serviceId + "], url = [" + url + "], name = ["
|
||||
+ title + "], playQueue = [" + playQueue + "]");
|
||||
}
|
||||
|
||||
@ -1848,13 +1858,16 @@ public final class VideoDetailFragment
|
||||
|
||||
@Override
|
||||
public void onServiceStopped() {
|
||||
setOverlayPlayPauseImage(false);
|
||||
if (currentInfo != null) {
|
||||
updateOverlayData(currentInfo.getName(),
|
||||
currentInfo.getUploaderName(),
|
||||
currentInfo.getThumbnails());
|
||||
// the binding could be null at this point, if the app is finishing
|
||||
if (binding != null) {
|
||||
setOverlayPlayPauseImage(false);
|
||||
if (currentInfo != null) {
|
||||
updateOverlayData(currentInfo.getName(),
|
||||
currentInfo.getUploaderName(),
|
||||
currentInfo.getThumbnails());
|
||||
}
|
||||
updateOverlayPlayQueueButtonVisibility();
|
||||
}
|
||||
updateOverlayPlayQueueButtonVisibility();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -120,67 +120,6 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
||||
// LifeCycle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
menuProvider = new MenuProvider() {
|
||||
@Override
|
||||
public void onCreateMenu(@NonNull final Menu menu,
|
||||
@NonNull final MenuInflater inflater) {
|
||||
inflater.inflate(R.menu.menu_channel, menu);
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onCreateOptionsMenu() called with: "
|
||||
+ "menu = [" + menu + "], inflater = [" + inflater + "]");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPrepareMenu(@NonNull final Menu menu) {
|
||||
menuRssButton = menu.findItem(R.id.menu_item_rss);
|
||||
menuNotifyButton = menu.findItem(R.id.menu_item_notify);
|
||||
updateRssButton();
|
||||
updateNotifyButton(channelSubscription);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMenuItemSelected(@NonNull final MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.menu_item_notify:
|
||||
final boolean value = !item.isChecked();
|
||||
item.setEnabled(false);
|
||||
setNotify(value);
|
||||
break;
|
||||
case R.id.action_settings:
|
||||
NavigationHelper.openSettings(requireContext());
|
||||
break;
|
||||
case R.id.menu_item_rss:
|
||||
if (currentInfo != null) {
|
||||
ShareUtils.openUrlInApp(requireContext(), currentInfo.getFeedUrl());
|
||||
}
|
||||
break;
|
||||
case R.id.menu_item_openInBrowser:
|
||||
if (currentInfo != null) {
|
||||
ShareUtils.openUrlInBrowser(requireContext(),
|
||||
currentInfo.getOriginalUrl());
|
||||
}
|
||||
break;
|
||||
case R.id.menu_item_share:
|
||||
if (currentInfo != null) {
|
||||
ShareUtils.shareText(requireContext(), name,
|
||||
currentInfo.getOriginalUrl(), currentInfo.getAvatars());
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
activity.addMenuProvider(menuProvider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(@NonNull final Context context) {
|
||||
super.onAttach(context);
|
||||
@ -195,6 +134,67 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull final View rootView, final Bundle savedInstanceState) {
|
||||
super.onViewCreated(rootView, savedInstanceState);
|
||||
menuProvider = new MenuProvider() {
|
||||
@Override
|
||||
public void onCreateMenu(@NonNull final Menu menu,
|
||||
@NonNull final MenuInflater inflater) {
|
||||
inflater.inflate(R.menu.menu_channel, menu);
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onCreateOptionsMenu() called with: "
|
||||
+ "menu = [" + menu + "], inflater = [" + inflater + "]");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPrepareMenu(@NonNull final Menu menu) {
|
||||
menuRssButton = menu.findItem(R.id.menu_item_rss);
|
||||
menuNotifyButton = menu.findItem(R.id.menu_item_notify);
|
||||
updateRssButton();
|
||||
updateNotifyButton(channelSubscription);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMenuItemSelected(@NonNull final MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.menu_item_notify:
|
||||
final boolean value = !item.isChecked();
|
||||
item.setEnabled(false);
|
||||
setNotify(value);
|
||||
break;
|
||||
case R.id.action_settings:
|
||||
NavigationHelper.openSettings(requireContext());
|
||||
break;
|
||||
case R.id.menu_item_rss:
|
||||
if (currentInfo != null) {
|
||||
ShareUtils.openUrlInApp(requireContext(), currentInfo.getFeedUrl());
|
||||
}
|
||||
break;
|
||||
case R.id.menu_item_openInBrowser:
|
||||
if (currentInfo != null) {
|
||||
ShareUtils.openUrlInBrowser(requireContext(),
|
||||
currentInfo.getOriginalUrl());
|
||||
}
|
||||
break;
|
||||
case R.id.menu_item_share:
|
||||
if (currentInfo != null) {
|
||||
ShareUtils.shareText(requireContext(), name,
|
||||
currentInfo.getOriginalUrl(), currentInfo.getAvatars());
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
activity.addMenuProvider(menuProvider);
|
||||
}
|
||||
|
||||
@Override // called from onViewCreated in BaseFragment.onViewCreated
|
||||
protected void initViews(final View rootView, final Bundle savedInstanceState) {
|
||||
super.initViews(rootView, savedInstanceState);
|
||||
@ -232,6 +232,14 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
||||
binding.subChannelTitleView.setOnClickListener(openSubChannel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
if (menuProvider != null) {
|
||||
activity.removeMenuProvider(menuProvider);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
@ -240,7 +248,6 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
||||
}
|
||||
disposables.clear();
|
||||
binding = null;
|
||||
activity.removeMenuProvider(menuProvider);
|
||||
menuProvider = null;
|
||||
}
|
||||
|
||||
|
@ -7,3 +7,16 @@ import androidx.core.os.BundleCompat
|
||||
inline fun <reified T : Parcelable> Bundle.parcelableArrayList(key: String?): ArrayList<T>? {
|
||||
return BundleCompat.getParcelableArrayList(this, key, T::class.java)
|
||||
}
|
||||
|
||||
fun Bundle?.toDebugString(): String {
|
||||
if (this == null) {
|
||||
return "null"
|
||||
}
|
||||
val string = StringBuilder("Bundle{")
|
||||
for (key in this.keySet()) {
|
||||
@Suppress("DEPRECATION") // we want this[key] to return items of any type
|
||||
string.append(" ").append(key).append(" => ").append(this[key]).append(";")
|
||||
}
|
||||
string.append(" }")
|
||||
return string.toString()
|
||||
}
|
||||
|
@ -17,8 +17,10 @@ import androidx.core.view.isGone
|
||||
import androidx.core.view.isInvisible
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
|
||||
import org.schabi.newpipe.MainActivity
|
||||
|
||||
// logs in this class are disabled by default since it's usually not useful,
|
||||
// you can enable them by setting this flag to MainActivity.DEBUG
|
||||
private const val DEBUG = false
|
||||
private const val TAG = "ViewUtils"
|
||||
|
||||
/**
|
||||
@ -38,7 +40,7 @@ fun View.animate(
|
||||
delay: Long = 0,
|
||||
execOnEnd: Runnable? = null
|
||||
) {
|
||||
if (MainActivity.DEBUG) {
|
||||
if (DEBUG) {
|
||||
val id = try {
|
||||
resources.getResourceEntryName(id)
|
||||
} catch (e: Exception) {
|
||||
@ -51,7 +53,7 @@ fun View.animate(
|
||||
Log.d(TAG, "animate(): $msg")
|
||||
}
|
||||
if (isVisible && enterOrExit) {
|
||||
if (MainActivity.DEBUG) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "animate(): view was already visible > view = [$this]")
|
||||
}
|
||||
animate().setListener(null).cancel()
|
||||
@ -60,7 +62,7 @@ fun View.animate(
|
||||
execOnEnd?.run()
|
||||
return
|
||||
} else if ((isGone || isInvisible) && !enterOrExit) {
|
||||
if (MainActivity.DEBUG) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "animate(): view was already gone > view = [$this]")
|
||||
}
|
||||
animate().setListener(null).cancel()
|
||||
@ -89,7 +91,7 @@ fun View.animate(
|
||||
* @param colorEnd the background color to end with
|
||||
*/
|
||||
fun View.animateBackgroundColor(duration: Long, @ColorInt colorStart: Int, @ColorInt colorEnd: Int) {
|
||||
if (MainActivity.DEBUG) {
|
||||
if (DEBUG) {
|
||||
Log.d(
|
||||
TAG,
|
||||
"animateBackgroundColor() called with: view = [$this], duration = [$duration], " +
|
||||
@ -109,7 +111,7 @@ fun View.animateBackgroundColor(duration: Long, @ColorInt colorStart: Int, @Colo
|
||||
}
|
||||
|
||||
fun View.animateHeight(duration: Long, targetHeight: Int): ValueAnimator {
|
||||
if (MainActivity.DEBUG) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "animateHeight: duration = [$duration], from $height to → $targetHeight in: $this")
|
||||
}
|
||||
val animator = ValueAnimator.ofFloat(height.toFloat(), targetHeight.toFloat())
|
||||
@ -127,7 +129,7 @@ fun View.animateHeight(duration: Long, targetHeight: Int): ValueAnimator {
|
||||
}
|
||||
|
||||
fun View.animateRotation(duration: Long, targetRotation: Int) {
|
||||
if (MainActivity.DEBUG) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "animateRotation: duration = [$duration], from $rotation to → $targetRotation in: $this")
|
||||
}
|
||||
animate().setListener(null).cancel()
|
||||
|
@ -88,7 +88,7 @@ class NotificationHelper(val context: Context) {
|
||||
|
||||
// Show individual stream notifications, set channel icon only if there is actually
|
||||
// one
|
||||
showStreamNotifications(newStreams, data.serviceId, bitmap)
|
||||
showStreamNotifications(newStreams, data.serviceId, data.url, bitmap)
|
||||
// Show summary notification
|
||||
manager.notify(data.pseudoId, summaryBuilder.build())
|
||||
|
||||
@ -97,7 +97,7 @@ class NotificationHelper(val context: Context) {
|
||||
|
||||
override fun onBitmapFailed(e: Exception, errorDrawable: Drawable) {
|
||||
// Show individual stream notifications
|
||||
showStreamNotifications(newStreams, data.serviceId, null)
|
||||
showStreamNotifications(newStreams, data.serviceId, data.url, null)
|
||||
// Show summary notification
|
||||
manager.notify(data.pseudoId, summaryBuilder.build())
|
||||
iconLoadingTargets.remove(this) // allow it to be garbage-collected
|
||||
@ -118,10 +118,11 @@ class NotificationHelper(val context: Context) {
|
||||
private fun showStreamNotifications(
|
||||
newStreams: List<StreamInfoItem>,
|
||||
serviceId: Int,
|
||||
channelUrl: String,
|
||||
channelIcon: Bitmap?
|
||||
) {
|
||||
for (stream in newStreams) {
|
||||
val notification = createStreamNotification(stream, serviceId, channelIcon)
|
||||
val notification = createStreamNotification(stream, serviceId, channelUrl, channelIcon)
|
||||
manager.notify(stream.url.hashCode(), notification)
|
||||
}
|
||||
}
|
||||
@ -129,6 +130,7 @@ class NotificationHelper(val context: Context) {
|
||||
private fun createStreamNotification(
|
||||
item: StreamInfoItem,
|
||||
serviceId: Int,
|
||||
channelUrl: String,
|
||||
channelIcon: Bitmap?
|
||||
): Notification {
|
||||
return NotificationCompat.Builder(
|
||||
@ -139,7 +141,7 @@ class NotificationHelper(val context: Context) {
|
||||
.setLargeIcon(channelIcon)
|
||||
.setContentTitle(item.name)
|
||||
.setContentText(item.uploaderName)
|
||||
.setGroup(item.uploaderUrl)
|
||||
.setGroup(channelUrl)
|
||||
.setColor(ContextCompat.getColor(context, R.color.ic_launcher_background))
|
||||
.setColorized(true)
|
||||
.setAutoCancel(true)
|
||||
|
@ -0,0 +1,72 @@
|
||||
package org.schabi.newpipe.local.playlist
|
||||
|
||||
import android.content.Context
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.database.playlist.PlaylistStreamEntry
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException
|
||||
import org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeStreamLinkHandlerFactory
|
||||
import org.schabi.newpipe.local.playlist.PlayListShareMode.JUST_URLS
|
||||
import org.schabi.newpipe.local.playlist.PlayListShareMode.WITH_TITLES
|
||||
import org.schabi.newpipe.local.playlist.PlayListShareMode.YOUTUBE_TEMP_PLAYLIST
|
||||
|
||||
fun export(
|
||||
shareMode: PlayListShareMode,
|
||||
playlist: List<PlaylistStreamEntry>,
|
||||
context: Context
|
||||
): String {
|
||||
return when (shareMode) {
|
||||
WITH_TITLES -> exportWithTitles(playlist, context)
|
||||
JUST_URLS -> exportJustUrls(playlist)
|
||||
YOUTUBE_TEMP_PLAYLIST -> exportAsYoutubeTempPlaylist(playlist)
|
||||
}
|
||||
}
|
||||
|
||||
fun exportWithTitles(
|
||||
playlist: List<PlaylistStreamEntry>,
|
||||
context: Context
|
||||
): String {
|
||||
|
||||
return playlist.asSequence()
|
||||
.map { it.streamEntity }
|
||||
.map { entity ->
|
||||
context.getString(
|
||||
R.string.video_details_list_item,
|
||||
entity.title,
|
||||
entity.url
|
||||
)
|
||||
}
|
||||
.joinToString(separator = "\n")
|
||||
}
|
||||
|
||||
fun exportJustUrls(playlist: List<PlaylistStreamEntry>): String {
|
||||
|
||||
return playlist.asSequence()
|
||||
.map { it.streamEntity.url }
|
||||
.joinToString(separator = "\n")
|
||||
}
|
||||
|
||||
fun exportAsYoutubeTempPlaylist(playlist: List<PlaylistStreamEntry>): String {
|
||||
|
||||
val videoIDs = playlist.asReversed().asSequence()
|
||||
.map { it.streamEntity.url }
|
||||
.mapNotNull(::getYouTubeId)
|
||||
.take(50) // YouTube limitation: temp playlists can't have more than 50 items
|
||||
.toList()
|
||||
.asReversed()
|
||||
.joinToString(separator = ",")
|
||||
|
||||
return "https://www.youtube.com/watch_videos?video_ids=$videoIDs"
|
||||
}
|
||||
|
||||
val linkHandler: YoutubeStreamLinkHandlerFactory = YoutubeStreamLinkHandlerFactory.getInstance()
|
||||
|
||||
/**
|
||||
* Gets the video id from a YouTube URL.
|
||||
*
|
||||
* @param url YouTube URL
|
||||
* @return the video id
|
||||
*/
|
||||
fun getYouTubeId(url: String): String? {
|
||||
|
||||
return try { linkHandler.getId(url) } catch (e: ParsingException) { null }
|
||||
}
|
@ -2,8 +2,13 @@ package org.schabi.newpipe.local.playlist;
|
||||
|
||||
import static org.schabi.newpipe.error.ErrorUtil.showUiErrorSnackbar;
|
||||
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
||||
import static org.schabi.newpipe.local.playlist.ExportPlaylistKt.export;
|
||||
import static org.schabi.newpipe.local.playlist.PlayListShareMode.JUST_URLS;
|
||||
import static org.schabi.newpipe.local.playlist.PlayListShareMode.WITH_TITLES;
|
||||
import static org.schabi.newpipe.local.playlist.PlayListShareMode.YOUTUBE_TEMP_PLAYLIST;
|
||||
import static org.schabi.newpipe.util.ThemeHelper.shouldUseGridLayout;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcelable;
|
||||
@ -27,7 +32,6 @@ import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.viewbinding.ViewBinding;
|
||||
|
||||
import com.evernote.android.state.State;
|
||||
|
||||
import org.reactivestreams.Subscriber;
|
||||
import org.reactivestreams.Subscription;
|
||||
import org.schabi.newpipe.NewPipeDatabase;
|
||||
@ -385,34 +389,41 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||
}
|
||||
|
||||
/**
|
||||
* Shares the playlist as a list of stream URLs if {@code shouldSharePlaylistDetails} is
|
||||
* set to {@code false}. Shares the playlist name along with a list of video titles and URLs
|
||||
* if {@code shouldSharePlaylistDetails} is set to {@code true}.
|
||||
* Shares the playlist in one of 3 ways, depending on the value of {@code shareMode}:
|
||||
* <ul>
|
||||
* <li>{@code JUST_URLS}: shares the URLs only.</li>
|
||||
* <li>{@code WITH_TITLES}: each entry in the list is accompanied by its title.</li>
|
||||
* <li>{@code YOUTUBE_TEMP_PLAYLIST}: shares as a YouTube temporary playlist.</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param shouldSharePlaylistDetails Whether the playlist details should be included in the
|
||||
* shared content.
|
||||
* @param shareMode The way the playlist should be shared.
|
||||
*/
|
||||
private void sharePlaylist(final boolean shouldSharePlaylistDetails) {
|
||||
private void sharePlaylist(final PlayListShareMode shareMode) {
|
||||
final Context context = requireContext();
|
||||
|
||||
disposables.add(playlistManager.getPlaylistStreams(playlistId)
|
||||
.flatMapSingle(playlist -> Single.just(playlist.stream()
|
||||
.map(PlaylistStreamEntry::getStreamEntity)
|
||||
.map(streamEntity -> {
|
||||
if (shouldSharePlaylistDetails) {
|
||||
return context.getString(R.string.video_details_list_item,
|
||||
streamEntity.getTitle(), streamEntity.getUrl());
|
||||
} else {
|
||||
return streamEntity.getUrl();
|
||||
}
|
||||
})
|
||||
.collect(Collectors.joining("\n"))))
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(urlsText -> ShareUtils.shareText(
|
||||
context, name, shouldSharePlaylistDetails
|
||||
? context.getString(R.string.share_playlist_content_details,
|
||||
name, urlsText) : urlsText),
|
||||
throwable -> showUiErrorSnackbar(this, "Sharing playlist", throwable)));
|
||||
.flatMapSingle(playlist -> Single.just(export(
|
||||
|
||||
shareMode,
|
||||
playlist,
|
||||
context
|
||||
)))
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
urlsText -> {
|
||||
|
||||
final String content = shareMode == WITH_TITLES
|
||||
? context.getString(R.string.share_playlist_content_details,
|
||||
name,
|
||||
urlsText
|
||||
)
|
||||
: urlsText;
|
||||
|
||||
ShareUtils.shareText(context, name, content);
|
||||
},
|
||||
throwable -> showUiErrorSnackbar(this, "Sharing playlist", throwable)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public void removeWatchedStreams(final boolean removePartiallyWatched) {
|
||||
@ -872,13 +883,15 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||
private void createShareConfirmationDialog() {
|
||||
new AlertDialog.Builder(requireContext())
|
||||
.setTitle(R.string.share_playlist)
|
||||
.setMessage(R.string.share_playlist_with_titles_message)
|
||||
.setCancelable(true)
|
||||
.setPositiveButton(R.string.share_playlist_with_titles, (dialog, which) ->
|
||||
sharePlaylist(/* shouldSharePlaylistDetails= */ true)
|
||||
sharePlaylist(WITH_TITLES)
|
||||
)
|
||||
.setNeutralButton(R.string.share_playlist_as_youtube_temporary_playlist,
|
||||
(dialog, which) -> sharePlaylist(YOUTUBE_TEMP_PLAYLIST)
|
||||
)
|
||||
.setNegativeButton(R.string.share_playlist_with_list, (dialog, which) ->
|
||||
sharePlaylist(/* shouldSharePlaylistDetails= */ false)
|
||||
sharePlaylist(JUST_URLS)
|
||||
)
|
||||
.show();
|
||||
}
|
||||
|
@ -0,0 +1,8 @@
|
||||
package org.schabi.newpipe.local.playlist;
|
||||
|
||||
public enum PlayListShareMode {
|
||||
|
||||
JUST_URLS,
|
||||
WITH_TITLES,
|
||||
YOUTUBE_TEMP_PLAYLIST
|
||||
}
|
@ -26,6 +26,10 @@ public class RemotePlaylistManager {
|
||||
return playlistRemoteTable.getPlaylists().subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
public Flowable<PlaylistRemoteEntity> getPlaylist(final long playlistId) {
|
||||
return playlistRemoteTable.getPlaylist(playlistId).subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
public Flowable<List<PlaylistRemoteEntity>> getPlaylist(final PlaylistInfo info) {
|
||||
return playlistRemoteTable.getPlaylist(info.getServiceId(), info.getUrl())
|
||||
.subscribeOn(Schedulers.io());
|
||||
|
@ -183,7 +183,10 @@ public final class PlayQueueActivity extends AppCompatActivity
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private void bind() {
|
||||
// Note: this code should not really exist, and PlayerHolder should be used instead, but
|
||||
// it will be rewritten when NewPlayer will replace the current player.
|
||||
final Intent bindIntent = new Intent(this, PlayerService.class);
|
||||
bindIntent.setAction(PlayerService.BIND_PLAYER_HOLDER_ACTION);
|
||||
final boolean success = bindService(bindIntent, serviceConnection, BIND_AUTO_CREATE);
|
||||
if (!success) {
|
||||
unbindService(serviceConnection);
|
||||
@ -221,7 +224,7 @@ public final class PlayQueueActivity extends AppCompatActivity
|
||||
Log.d(TAG, "Player service is connected");
|
||||
|
||||
if (service instanceof PlayerService.LocalBinder) {
|
||||
player = ((PlayerService.LocalBinder) service).getPlayer();
|
||||
player = ((PlayerService.LocalBinder) service).getService().getPlayer();
|
||||
}
|
||||
|
||||
if (player == null || player.getPlayQueue() == null || player.exoPlayerIsNull()) {
|
||||
|
@ -55,6 +55,7 @@ import android.content.SharedPreferences;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.media.AudioManager;
|
||||
import android.support.v4.media.session.MediaSessionCompat;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
|
||||
@ -71,6 +72,7 @@ import com.google.android.exoplayer2.PlaybackParameters;
|
||||
import com.google.android.exoplayer2.Player.PositionInfo;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.Tracks;
|
||||
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.text.CueGroup;
|
||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||
@ -269,7 +271,16 @@ public final class Player implements PlaybackListener, Listener {
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
//region Constructor
|
||||
|
||||
public Player(@NonNull final PlayerService service) {
|
||||
/**
|
||||
* @param service the service this player resides in
|
||||
* @param mediaSession used to build the {@link MediaSessionPlayerUi}, lives in the service and
|
||||
* could possibly be reused with multiple player instances
|
||||
* @param sessionConnector used to build the {@link MediaSessionPlayerUi}, lives in the service
|
||||
* and could possibly be reused with multiple player instances
|
||||
*/
|
||||
public Player(@NonNull final PlayerService service,
|
||||
@NonNull final MediaSessionCompat mediaSession,
|
||||
@NonNull final MediaSessionConnector sessionConnector) {
|
||||
this.service = service;
|
||||
context = service;
|
||||
prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
@ -302,7 +313,7 @@ public final class Player implements PlaybackListener, Listener {
|
||||
// notification ui in the UIs list, since the notification depends on the media session in
|
||||
// PlayerUi#initPlayer(), and UIs.call() guarantees UI order is preserved.
|
||||
UIs = new PlayerUiList(
|
||||
new MediaSessionPlayerUi(this),
|
||||
new MediaSessionPlayerUi(this, mediaSession, sessionConnector),
|
||||
new NotificationPlayerUi(this)
|
||||
);
|
||||
}
|
||||
@ -646,7 +657,7 @@ public final class Player implements PlaybackListener, Listener {
|
||||
Log.d(TAG, "onPlaybackShutdown() called");
|
||||
}
|
||||
// destroys the service, which in turn will destroy the player
|
||||
service.stopService();
|
||||
service.destroyPlayerAndStopService();
|
||||
}
|
||||
|
||||
public void smoothStopForImmediateReusing() {
|
||||
@ -718,7 +729,7 @@ public final class Player implements PlaybackListener, Listener {
|
||||
pause();
|
||||
break;
|
||||
case ACTION_CLOSE:
|
||||
service.stopService();
|
||||
service.destroyPlayerAndStopService();
|
||||
break;
|
||||
case ACTION_PLAY_PAUSE:
|
||||
playPause();
|
||||
@ -1375,6 +1386,19 @@ public final class Player implements PlaybackListener, Listener {
|
||||
public void onCues(@NonNull final CueGroup cueGroup) {
|
||||
UIs.call(playerUi -> playerUi.onCues(cueGroup.cues));
|
||||
}
|
||||
|
||||
/**
|
||||
* To be called when the {@code PlaybackPreparer} set in the {@link MediaSessionConnector}
|
||||
* receives an {@code onPrepare()} call. This function allows restoring the default behavior
|
||||
* that would happen if there was no playback preparer set, i.e. to just call
|
||||
* {@code player.prepare()}. You can find the default behavior in `onPlay()` inside the
|
||||
* {@link MediaSessionConnector} file.
|
||||
*/
|
||||
public void onPrepare() {
|
||||
if (!exoPlayerIsNull()) {
|
||||
simpleExoPlayer.prepare();
|
||||
}
|
||||
}
|
||||
//endregion
|
||||
|
||||
|
||||
|
@ -21,75 +21,142 @@ package org.schabi.newpipe.player;
|
||||
|
||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
|
||||
import android.app.Service;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Binder;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
import android.support.v4.media.MediaBrowserCompat;
|
||||
import android.support.v4.media.session.MediaSessionCompat;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.app.ServiceCompat;
|
||||
import androidx.media.MediaBrowserServiceCompat;
|
||||
|
||||
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
|
||||
|
||||
import org.schabi.newpipe.ktx.BundleKt;
|
||||
import org.schabi.newpipe.player.mediabrowser.MediaBrowserImpl;
|
||||
import org.schabi.newpipe.player.mediabrowser.MediaBrowserPlaybackPreparer;
|
||||
import org.schabi.newpipe.player.mediasession.MediaSessionPlayerUi;
|
||||
import org.schabi.newpipe.player.notification.NotificationPlayerUi;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
|
||||
/**
|
||||
* One service for all players.
|
||||
*/
|
||||
public final class PlayerService extends Service {
|
||||
public final class PlayerService extends MediaBrowserServiceCompat {
|
||||
private static final String TAG = PlayerService.class.getSimpleName();
|
||||
private static final boolean DEBUG = Player.DEBUG;
|
||||
|
||||
public static final String SHOULD_START_FOREGROUND_EXTRA = "should_start_foreground_extra";
|
||||
public static final String BIND_PLAYER_HOLDER_ACTION = "bind_player_holder_action";
|
||||
|
||||
// These objects are used to cleanly separate the Service implementation (in this file) and the
|
||||
// media browser and playback preparer implementations. At the moment the playback preparer is
|
||||
// only used in conjunction with the media browser.
|
||||
private MediaBrowserImpl mediaBrowserImpl;
|
||||
private MediaBrowserPlaybackPreparer mediaBrowserPlaybackPreparer;
|
||||
|
||||
// these are instantiated in onCreate() as per
|
||||
// https://developer.android.com/training/cars/media#browser_workflow
|
||||
private MediaSessionCompat mediaSession;
|
||||
private MediaSessionConnector sessionConnector;
|
||||
|
||||
@Nullable
|
||||
private Player player;
|
||||
|
||||
private final IBinder mBinder = new PlayerService.LocalBinder(this);
|
||||
|
||||
/**
|
||||
* The parameter taken by this {@link Consumer} can be null to indicate the player is being
|
||||
* stopped.
|
||||
*/
|
||||
@Nullable
|
||||
private Consumer<Player> onPlayerStartedOrStopped = null;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Service's LifeCycle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
//region Service lifecycle
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onCreate() called");
|
||||
}
|
||||
assureCorrectAppLanguage(this);
|
||||
ThemeHelper.setTheme(this);
|
||||
|
||||
player = new Player(this);
|
||||
/*
|
||||
Create the player notification and start immediately the service in foreground,
|
||||
otherwise if nothing is played or initializing the player and its components (especially
|
||||
loading stream metadata) takes a lot of time, the app would crash on Android 8+ as the
|
||||
service would never be put in the foreground while we said to the system we would do so
|
||||
*/
|
||||
player.UIs().get(NotificationPlayerUi.class)
|
||||
.ifPresent(NotificationPlayerUi::createNotificationAndStartForeground);
|
||||
mediaBrowserImpl = new MediaBrowserImpl(this, this::notifyChildrenChanged);
|
||||
|
||||
// see https://developer.android.com/training/cars/media#browser_workflow
|
||||
mediaSession = new MediaSessionCompat(this, "MediaSessionPlayerServ");
|
||||
setSessionToken(mediaSession.getSessionToken());
|
||||
sessionConnector = new MediaSessionConnector(mediaSession);
|
||||
sessionConnector.setMetadataDeduplicationEnabled(true);
|
||||
|
||||
mediaBrowserPlaybackPreparer = new MediaBrowserPlaybackPreparer(
|
||||
this,
|
||||
sessionConnector::setCustomErrorMessage,
|
||||
() -> sessionConnector.setCustomErrorMessage(null),
|
||||
(playWhenReady) -> {
|
||||
if (player != null) {
|
||||
player.onPrepare();
|
||||
}
|
||||
}
|
||||
);
|
||||
sessionConnector.setPlaybackPreparer(mediaBrowserPlaybackPreparer);
|
||||
|
||||
// Note: you might be tempted to create the player instance and call startForeground here,
|
||||
// but be aware that the Android system might start the service just to perform media
|
||||
// queries. In those cases creating a player instance is a waste of resources, and calling
|
||||
// startForeground means creating a useless empty notification. In case it's really needed
|
||||
// the player instance can be created here, but startForeground() should definitely not be
|
||||
// called here unless the service is actually starting in the foreground, to avoid the
|
||||
// useless notification.
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(final Intent intent, final int flags, final int startId) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onStartCommand() called with: intent = [" + intent
|
||||
+ "], extras = [" + BundleKt.toDebugString(intent.getExtras())
|
||||
+ "], flags = [" + flags + "], startId = [" + startId + "]");
|
||||
}
|
||||
|
||||
/*
|
||||
Be sure that the player notification is set and the service is started in foreground,
|
||||
otherwise, the app may crash on Android 8+ as the service would never be put in the
|
||||
foreground while we said to the system we would do so
|
||||
The service is always requested to be started in foreground, so always creating a
|
||||
notification if there is no one already and starting the service in foreground should
|
||||
not create any issues
|
||||
If the service is already started in foreground, requesting it to be started shouldn't
|
||||
do anything
|
||||
*/
|
||||
if (player != null) {
|
||||
// All internal NewPipe intents used to interact with the player, that are sent to the
|
||||
// PlayerService using startForegroundService(), will have SHOULD_START_FOREGROUND_EXTRA,
|
||||
// to ensure startForeground() is called (otherwise Android will force-crash the app).
|
||||
if (intent.getBooleanExtra(SHOULD_START_FOREGROUND_EXTRA, false)) {
|
||||
final boolean playerWasNull = (player == null);
|
||||
if (playerWasNull) {
|
||||
// make sure the player exists, in case the service was resumed
|
||||
player = new Player(this, mediaSession, sessionConnector);
|
||||
}
|
||||
|
||||
// Be sure that the player notification is set and the service is started in foreground,
|
||||
// otherwise, the app may crash on Android 8+ as the service would never be put in the
|
||||
// foreground while we said to the system we would do so. The service is always
|
||||
// requested to be started in foreground, so always creating a notification if there is
|
||||
// no one already and starting the service in foreground should not create any issues.
|
||||
// If the service is already started in foreground, requesting it to be started
|
||||
// shouldn't do anything.
|
||||
player.UIs().get(NotificationPlayerUi.class)
|
||||
.ifPresent(NotificationPlayerUi::createNotificationAndStartForeground);
|
||||
|
||||
if (playerWasNull && onPlayerStartedOrStopped != null) {
|
||||
// notify that a new player was created (but do it after creating the foreground
|
||||
// notification just to make sure we don't incur, due to slowness, in
|
||||
// "Context.startForegroundService() did not then call Service.startForeground()")
|
||||
onPlayerStartedOrStopped.accept(player);
|
||||
}
|
||||
}
|
||||
|
||||
if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())
|
||||
@ -100,7 +167,7 @@ public final class PlayerService extends Service {
|
||||
Stop the service in this case, which will be removed from the foreground and its
|
||||
notification cancelled in its destruction
|
||||
*/
|
||||
stopSelf();
|
||||
destroyPlayerAndStopService();
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
@ -142,29 +209,84 @@ public final class PlayerService extends Service {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "destroy() called");
|
||||
}
|
||||
super.onDestroy();
|
||||
|
||||
cleanup();
|
||||
|
||||
mediaBrowserPlaybackPreparer.dispose();
|
||||
mediaSession.release();
|
||||
mediaBrowserImpl.dispose();
|
||||
}
|
||||
|
||||
private void cleanup() {
|
||||
if (player != null) {
|
||||
if (onPlayerStartedOrStopped != null) {
|
||||
// notify that the player is being destroyed
|
||||
onPlayerStartedOrStopped.accept(null);
|
||||
}
|
||||
player.destroy();
|
||||
player = null;
|
||||
}
|
||||
|
||||
// Should already be handled by MediaSessionPlayerUi, but just to be sure.
|
||||
mediaSession.setActive(false);
|
||||
|
||||
// Should already be handled by NotificationUtil.cancelNotificationAndStopForeground() in
|
||||
// NotificationPlayerUi, but let's make sure that the foreground service is stopped.
|
||||
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE);
|
||||
}
|
||||
|
||||
public void stopService() {
|
||||
/**
|
||||
* Destroys the player and allows the player instance to be garbage collected. Sets the media
|
||||
* session to inactive. Stops the foreground service and removes the player notification
|
||||
* associated with it. Tries to stop the {@link PlayerService} completely, but this step will
|
||||
* have no effect in case some service connection still uses the service (e.g. the Android Auto
|
||||
* system accesses the media browser even when no player is running).
|
||||
*/
|
||||
public void destroyPlayerAndStopService() {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "destroyPlayerAndStopService() called");
|
||||
}
|
||||
|
||||
cleanup();
|
||||
stopSelf();
|
||||
|
||||
// This only really stops the service if there are no other service connections (see docs):
|
||||
// for example the (Android Auto) media browser binder will block stopService().
|
||||
// This is why we also stopForeground() above, to make sure the notification is removed.
|
||||
// If we were to call stopSelf(), then the service would be surely stopped (regardless of
|
||||
// other service connections), but this would be a waste of resources since the service
|
||||
// would be immediately restarted by those same connections to perform the queries.
|
||||
stopService(new Intent(this, PlayerService.class));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void attachBaseContext(final Context base) {
|
||||
super.attachBaseContext(AudioServiceLeakFix.preventLeakOf(base));
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region Bind
|
||||
@Override
|
||||
public IBinder onBind(final Intent intent) {
|
||||
return mBinder;
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onBind() called with: intent = [" + intent
|
||||
+ "], extras = [" + BundleKt.toDebugString(intent.getExtras()) + "]");
|
||||
}
|
||||
|
||||
if (BIND_PLAYER_HOLDER_ACTION.equals(intent.getAction())) {
|
||||
// Note that this binder might be reused multiple times while the service is alive, even
|
||||
// after unbind() has been called: https://stackoverflow.com/a/8794930 .
|
||||
return mBinder;
|
||||
|
||||
} else if (MediaBrowserServiceCompat.SERVICE_INTERFACE.equals(intent.getAction())) {
|
||||
// MediaBrowserService also uses its own binder, so for actions related to the media
|
||||
// browser service, pass the onBind to the superclass.
|
||||
return super.onBind(intent);
|
||||
|
||||
} else {
|
||||
// This is an unknown request, avoid returning any binder to not leak objects.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static class LocalBinder extends Binder {
|
||||
@ -177,9 +299,52 @@ public final class PlayerService extends Service {
|
||||
public PlayerService getService() {
|
||||
return playerService.get();
|
||||
}
|
||||
}
|
||||
|
||||
public Player getPlayer() {
|
||||
return playerService.get().player;
|
||||
/**
|
||||
* @return the current active player instance. May be null, since the player service can outlive
|
||||
* the player e.g. to respond to Android Auto media browser queries.
|
||||
*/
|
||||
@Nullable
|
||||
public Player getPlayer() {
|
||||
return player;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the listener that will be called when the player is started or stopped. If a
|
||||
* {@code null} listener is passed, then the current listener will be unset. The parameter taken
|
||||
* by the {@link Consumer} can be null to indicate that the player is stopping.
|
||||
* @param listener the listener to set or unset
|
||||
*/
|
||||
public void setPlayerListener(@Nullable final Consumer<Player> listener) {
|
||||
this.onPlayerStartedOrStopped = listener;
|
||||
if (listener != null) {
|
||||
// if there is no player, then `null` will be sent here, to ensure the state is synced
|
||||
listener.accept(player);
|
||||
}
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region Media browser
|
||||
@Override
|
||||
public BrowserRoot onGetRoot(@NonNull final String clientPackageName,
|
||||
final int clientUid,
|
||||
@Nullable final Bundle rootHints) {
|
||||
// TODO check if the accessing package has permission to view data
|
||||
return mediaBrowserImpl.onGetRoot(clientPackageName, clientUid, rootHints);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadChildren(@NonNull final String parentId,
|
||||
@NonNull final Result<List<MediaBrowserCompat.MediaItem>> result) {
|
||||
mediaBrowserImpl.onLoadChildren(parentId, result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSearch(@NonNull final String query,
|
||||
final Bundle extras,
|
||||
@NonNull final Result<List<MediaBrowserCompat.MediaItem>> result) {
|
||||
mediaBrowserImpl.onSearch(query, result);
|
||||
}
|
||||
//endregion
|
||||
}
|
||||
|
@ -14,10 +14,12 @@ import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
|
||||
import static com.google.android.exoplayer2.util.Util.castNonNull;
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getAndroidUserAgent;
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getIosUserAgent;
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getTvHtml5UserAgent;
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.isAndroidStreamingUrl;
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.isIosStreamingUrl;
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.isTvHtml5StreamingUrl;
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.isWebStreamingUrl;
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.isTvHtml5SimplyEmbeddedPlayerStreamingUrl;
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.isWebEmbeddedPlayerStreamingUrl;
|
||||
import static java.lang.Math.min;
|
||||
|
||||
import android.net.Uri;
|
||||
@ -270,6 +272,7 @@ public final class YoutubeHttpDataSource extends BaseDataSource implements HttpD
|
||||
|
||||
private static final String RN_PARAMETER = "&rn=";
|
||||
private static final String YOUTUBE_BASE_URL = "https://www.youtube.com";
|
||||
private static final byte[] POST_BODY = new byte[] {0x78, 0};
|
||||
|
||||
private final boolean allowCrossProtocolRedirects;
|
||||
private final boolean rangeParameterEnabled;
|
||||
@ -658,8 +661,11 @@ public final class YoutubeHttpDataSource extends BaseDataSource implements HttpD
|
||||
}
|
||||
}
|
||||
|
||||
final boolean isTvHtml5StreamingUrl = isTvHtml5StreamingUrl(requestUrl);
|
||||
|
||||
if (isWebStreamingUrl(requestUrl)
|
||||
|| isTvHtml5SimplyEmbeddedPlayerStreamingUrl(requestUrl)) {
|
||||
|| isTvHtml5StreamingUrl
|
||||
|| isWebEmbeddedPlayerStreamingUrl(requestUrl)) {
|
||||
httpURLConnection.setRequestProperty(HttpHeaders.ORIGIN, YOUTUBE_BASE_URL);
|
||||
httpURLConnection.setRequestProperty(HttpHeaders.REFERER, YOUTUBE_BASE_URL);
|
||||
httpURLConnection.setRequestProperty(HttpHeaders.SEC_FETCH_DEST, "empty");
|
||||
@ -679,6 +685,9 @@ public final class YoutubeHttpDataSource extends BaseDataSource implements HttpD
|
||||
} else if (isIosStreamingUrl) {
|
||||
httpURLConnection.setRequestProperty(HttpHeaders.USER_AGENT,
|
||||
getIosUserAgent(null));
|
||||
} else if (isTvHtml5StreamingUrl) {
|
||||
httpURLConnection.setRequestProperty(HttpHeaders.USER_AGENT,
|
||||
getTvHtml5UserAgent());
|
||||
} else {
|
||||
// non-mobile user agent
|
||||
httpURLConnection.setRequestProperty(HttpHeaders.USER_AGENT, DownloaderImpl.USER_AGENT);
|
||||
@ -687,22 +696,16 @@ public final class YoutubeHttpDataSource extends BaseDataSource implements HttpD
|
||||
httpURLConnection.setRequestProperty(HttpHeaders.ACCEPT_ENCODING,
|
||||
allowGzip ? "gzip" : "identity");
|
||||
httpURLConnection.setInstanceFollowRedirects(followRedirects);
|
||||
httpURLConnection.setDoOutput(httpBody != null);
|
||||
// Most clients use POST requests to fetch contents
|
||||
httpURLConnection.setRequestMethod("POST");
|
||||
httpURLConnection.setDoOutput(true);
|
||||
httpURLConnection.setFixedLengthStreamingMode(POST_BODY.length);
|
||||
httpURLConnection.connect();
|
||||
|
||||
// Mobile clients uses POST requests to fetch contents
|
||||
httpURLConnection.setRequestMethod(isAndroidStreamingUrl || isIosStreamingUrl
|
||||
? "POST"
|
||||
: DataSpec.getStringForHttpMethod(httpMethod));
|
||||
final OutputStream os = httpURLConnection.getOutputStream();
|
||||
os.write(POST_BODY);
|
||||
os.close();
|
||||
|
||||
if (httpBody != null) {
|
||||
httpURLConnection.setFixedLengthStreamingMode(httpBody.length);
|
||||
httpURLConnection.connect();
|
||||
final OutputStream os = httpURLConnection.getOutputStream();
|
||||
os.write(httpBody);
|
||||
os.close();
|
||||
} else {
|
||||
httpURLConnection.connect();
|
||||
}
|
||||
return httpURLConnection;
|
||||
}
|
||||
|
||||
|
@ -1,11 +1,48 @@
|
||||
package org.schabi.newpipe.player.event;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.schabi.newpipe.player.PlayerService;
|
||||
import org.schabi.newpipe.player.Player;
|
||||
|
||||
/**
|
||||
* In addition to {@link PlayerServiceEventListener}, provides callbacks for service and player
|
||||
* connections and disconnections. "Connected" here means that the service (resp. the
|
||||
* player) is running and is bound to {@link org.schabi.newpipe.player.helper.PlayerHolder}.
|
||||
* "Disconnected" means that either the service (resp. the player) was stopped completely, or that
|
||||
* {@link org.schabi.newpipe.player.helper.PlayerHolder} is not bound.
|
||||
*/
|
||||
public interface PlayerServiceExtendedEventListener extends PlayerServiceEventListener {
|
||||
void onServiceConnected(Player player,
|
||||
PlayerService playerService,
|
||||
boolean playAfterConnect);
|
||||
/**
|
||||
* The player service just connected to {@link org.schabi.newpipe.player.helper.PlayerHolder},
|
||||
* but the player may not be active at this moment, e.g. in case the service is running to
|
||||
* respond to Android Auto media browser queries without playing anything.
|
||||
* {@link #onPlayerConnected(Player, boolean)} will be called right after this function if there
|
||||
* is a player.
|
||||
*
|
||||
* @param playerService the newly connected player service
|
||||
*/
|
||||
void onServiceConnected(@NonNull PlayerService playerService);
|
||||
|
||||
/**
|
||||
* The player service is already connected and the player was just started.
|
||||
*
|
||||
* @param player the newly connected or started player
|
||||
* @param playAfterConnect whether to open the video player in the video details fragment
|
||||
*/
|
||||
void onPlayerConnected(@NonNull Player player, boolean playAfterConnect);
|
||||
|
||||
/**
|
||||
* The player got disconnected, for one of these reasons: the player is getting closed while
|
||||
* leaving the service open for future media browser queries, the service is stopping
|
||||
* completely, or {@link org.schabi.newpipe.player.helper.PlayerHolder} is unbinding.
|
||||
*/
|
||||
void onPlayerDisconnected();
|
||||
|
||||
/**
|
||||
* The service got disconnected from {@link org.schabi.newpipe.player.helper.PlayerHolder},
|
||||
* either because {@link org.schabi.newpipe.player.helper.PlayerHolder} is unbinding or because
|
||||
* the service is stopping completely.
|
||||
*/
|
||||
void onServiceDisconnected();
|
||||
}
|
||||
|
@ -22,6 +22,10 @@ import org.schabi.newpipe.player.PlayerType;
|
||||
import org.schabi.newpipe.player.event.PlayerServiceEventListener;
|
||||
import org.schabi.newpipe.player.event.PlayerServiceExtendedEventListener;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public final class PlayerHolder {
|
||||
|
||||
@ -44,7 +48,16 @@ public final class PlayerHolder {
|
||||
private final PlayerServiceConnection serviceConnection = new PlayerServiceConnection();
|
||||
private boolean bound;
|
||||
@Nullable private PlayerService playerService;
|
||||
@Nullable private Player player;
|
||||
|
||||
private Optional<Player> getPlayer() {
|
||||
return Optional.ofNullable(playerService)
|
||||
.flatMap(s -> Optional.ofNullable(s.getPlayer()));
|
||||
}
|
||||
|
||||
private Optional<PlayQueue> getPlayQueue() {
|
||||
// player play queue might be null e.g. while player is starting
|
||||
return getPlayer().flatMap(p -> Optional.ofNullable(p.getPlayQueue()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current {@link PlayerType} of the {@link PlayerService} service,
|
||||
@ -54,21 +67,15 @@ public final class PlayerHolder {
|
||||
*/
|
||||
@Nullable
|
||||
public PlayerType getType() {
|
||||
if (player == null) {
|
||||
return null;
|
||||
}
|
||||
return player.getPlayerType();
|
||||
return getPlayer().map(Player::getPlayerType).orElse(null);
|
||||
}
|
||||
|
||||
public boolean isPlaying() {
|
||||
if (player == null) {
|
||||
return false;
|
||||
}
|
||||
return player.isPlaying();
|
||||
return getPlayer().map(Player::isPlaying).orElse(false);
|
||||
}
|
||||
|
||||
public boolean isPlayerOpen() {
|
||||
return player != null;
|
||||
return getPlayer().isPresent();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -77,7 +84,7 @@ public final class PlayerHolder {
|
||||
* @return true only if the player is open and its play queue is ready (i.e. it is not null)
|
||||
*/
|
||||
public boolean isPlayQueueReady() {
|
||||
return player != null && player.getPlayQueue() != null;
|
||||
return getPlayQueue().isPresent();
|
||||
}
|
||||
|
||||
public boolean isBound() {
|
||||
@ -85,18 +92,11 @@ public final class PlayerHolder {
|
||||
}
|
||||
|
||||
public int getQueueSize() {
|
||||
if (player == null || player.getPlayQueue() == null) {
|
||||
// player play queue might be null e.g. while player is starting
|
||||
return 0;
|
||||
}
|
||||
return player.getPlayQueue().size();
|
||||
return getPlayQueue().map(PlayQueue::size).orElse(0);
|
||||
}
|
||||
|
||||
public int getQueuePosition() {
|
||||
if (player == null || player.getPlayQueue() == null) {
|
||||
return 0;
|
||||
}
|
||||
return player.getPlayQueue().getIndex();
|
||||
return getPlayQueue().map(PlayQueue::getIndex).orElse(0);
|
||||
}
|
||||
|
||||
public void setListener(@Nullable final PlayerServiceExtendedEventListener newListener) {
|
||||
@ -107,9 +107,10 @@ public final class PlayerHolder {
|
||||
}
|
||||
|
||||
// Force reload data from service
|
||||
if (player != null) {
|
||||
listener.onServiceConnected(player, playerService, false);
|
||||
if (playerService != null) {
|
||||
listener.onServiceConnected(playerService);
|
||||
startPlayerListener();
|
||||
// ^ will call listener.onPlayerConnected() down the line if there is an active player
|
||||
}
|
||||
}
|
||||
|
||||
@ -121,6 +122,9 @@ public final class PlayerHolder {
|
||||
|
||||
public void startService(final boolean playAfterConnect,
|
||||
final PlayerServiceExtendedEventListener newListener) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "startService() called with playAfterConnect=" + playAfterConnect);
|
||||
}
|
||||
final Context context = getCommonContext();
|
||||
setListener(newListener);
|
||||
if (bound) {
|
||||
@ -130,14 +134,24 @@ public final class PlayerHolder {
|
||||
// and NullPointerExceptions inside the service because the service will be
|
||||
// bound twice. Prevent it with unbinding first
|
||||
unbind(context);
|
||||
ContextCompat.startForegroundService(context, new Intent(context, PlayerService.class));
|
||||
final Intent intent = new Intent(context, PlayerService.class);
|
||||
intent.putExtra(PlayerService.SHOULD_START_FOREGROUND_EXTRA, true);
|
||||
ContextCompat.startForegroundService(context, intent);
|
||||
serviceConnection.doPlayAfterConnect(playAfterConnect);
|
||||
bind(context);
|
||||
}
|
||||
|
||||
public void stopService() {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "stopService() called");
|
||||
}
|
||||
if (playerService != null) {
|
||||
playerService.destroyPlayerAndStopService();
|
||||
}
|
||||
final Context context = getCommonContext();
|
||||
unbind(context);
|
||||
// destroyPlayerAndStopService() already runs the next line of code, but run it again just
|
||||
// to make sure to stop the service even if playerService is null by any chance.
|
||||
context.stopService(new Intent(context, PlayerService.class));
|
||||
}
|
||||
|
||||
@ -167,11 +181,16 @@ public final class PlayerHolder {
|
||||
final PlayerService.LocalBinder localBinder = (PlayerService.LocalBinder) service;
|
||||
|
||||
playerService = localBinder.getService();
|
||||
player = localBinder.getPlayer();
|
||||
if (listener != null) {
|
||||
listener.onServiceConnected(player, playerService, playAfterConnect);
|
||||
listener.onServiceConnected(playerService);
|
||||
getPlayer().ifPresent(p -> listener.onPlayerConnected(p, playAfterConnect));
|
||||
}
|
||||
startPlayerListener();
|
||||
// ^ will call listener.onPlayerConnected() down the line if there is an active player
|
||||
|
||||
// notify the main activity that binding the service has completed, so that it can
|
||||
// open the bottom mini-player
|
||||
NavigationHelper.sendPlayerStartedEvent(localBinder.getService());
|
||||
}
|
||||
}
|
||||
|
||||
@ -179,15 +198,28 @@ public final class PlayerHolder {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "bind() called");
|
||||
}
|
||||
|
||||
final Intent serviceIntent = new Intent(context, PlayerService.class);
|
||||
bound = context.bindService(serviceIntent, serviceConnection,
|
||||
Context.BIND_AUTO_CREATE);
|
||||
// BIND_AUTO_CREATE starts the service if it's not already running
|
||||
bound = bind(context, Context.BIND_AUTO_CREATE);
|
||||
if (!bound) {
|
||||
context.unbindService(serviceConnection);
|
||||
}
|
||||
}
|
||||
|
||||
public void tryBindIfNeeded(final Context context) {
|
||||
if (!bound) {
|
||||
// flags=0 means the service will not be started if it does not already exist. In this
|
||||
// case the return value is not useful, as a value of "true" does not really indicate
|
||||
// that the service is going to be bound.
|
||||
bind(context, 0);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean bind(final Context context, final int flags) {
|
||||
final Intent serviceIntent = new Intent(context, PlayerService.class);
|
||||
serviceIntent.setAction(PlayerService.BIND_PLAYER_HOLDER_ACTION);
|
||||
return context.bindService(serviceIntent, serviceConnection, flags);
|
||||
}
|
||||
|
||||
private void unbind(final Context context) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "unbind() called");
|
||||
@ -198,25 +230,32 @@ public final class PlayerHolder {
|
||||
bound = false;
|
||||
stopPlayerListener();
|
||||
playerService = null;
|
||||
player = null;
|
||||
if (listener != null) {
|
||||
listener.onPlayerDisconnected();
|
||||
listener.onServiceDisconnected();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void startPlayerListener() {
|
||||
if (player != null) {
|
||||
player.setFragmentListener(internalListener);
|
||||
if (playerService != null) {
|
||||
// setting the player listener will take care of calling relevant callbacks if the
|
||||
// player in the service is (not) already active, also see playerStateListener below
|
||||
playerService.setPlayerListener(playerStateListener);
|
||||
}
|
||||
getPlayer().ifPresent(p -> p.setFragmentListener(internalListener));
|
||||
}
|
||||
|
||||
private void stopPlayerListener() {
|
||||
if (player != null) {
|
||||
player.removeFragmentListener(internalListener);
|
||||
if (playerService != null) {
|
||||
playerService.setPlayerListener(null);
|
||||
}
|
||||
getPlayer().ifPresent(p -> p.removeFragmentListener(internalListener));
|
||||
}
|
||||
|
||||
/**
|
||||
* This listener will be held by the players created by {@link PlayerService}.
|
||||
*/
|
||||
private final PlayerServiceEventListener internalListener =
|
||||
new PlayerServiceEventListener() {
|
||||
@Override
|
||||
@ -303,4 +342,23 @@ public final class PlayerHolder {
|
||||
unbind(getCommonContext());
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* This listener will be held by bound {@link PlayerService}s to notify of the player starting
|
||||
* or stopping. This is necessary since the service outlives the player e.g. to answer Android
|
||||
* Auto media browser queries.
|
||||
*/
|
||||
private final Consumer<Player> playerStateListener = (@Nullable final Player player) -> {
|
||||
if (listener != null) {
|
||||
if (player == null) {
|
||||
// player.fragmentListener=null is already done by player.stopActivityBinding(),
|
||||
// which is called by player.destroy(), which is in turn called by PlayerService
|
||||
// before setting its player to null
|
||||
listener.onPlayerDisconnected();
|
||||
} else {
|
||||
listener.onPlayerConnected(player, serviceConnection.playAfterConnect);
|
||||
player.setFragmentListener(internalListener);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -0,0 +1,40 @@
|
||||
package org.schabi.newpipe.player.mediabrowser
|
||||
|
||||
import org.schabi.newpipe.BuildConfig
|
||||
import org.schabi.newpipe.extractor.InfoItem.InfoType
|
||||
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException
|
||||
|
||||
internal const val ID_AUTHORITY = BuildConfig.APPLICATION_ID
|
||||
internal const val ID_ROOT = "//$ID_AUTHORITY"
|
||||
internal const val ID_BOOKMARKS = "playlists"
|
||||
internal const val ID_HISTORY = "history"
|
||||
internal const val ID_INFO_ITEM = "item"
|
||||
|
||||
internal const val ID_LOCAL = "local"
|
||||
internal const val ID_REMOTE = "remote"
|
||||
internal const val ID_URL = "url"
|
||||
internal const val ID_STREAM = "stream"
|
||||
internal const val ID_PLAYLIST = "playlist"
|
||||
internal const val ID_CHANNEL = "channel"
|
||||
|
||||
internal fun infoItemTypeToString(type: InfoType): String {
|
||||
return when (type) {
|
||||
InfoType.STREAM -> ID_STREAM
|
||||
InfoType.PLAYLIST -> ID_PLAYLIST
|
||||
InfoType.CHANNEL -> ID_CHANNEL
|
||||
else -> throw IllegalStateException("Unexpected value: $type")
|
||||
}
|
||||
}
|
||||
|
||||
internal fun infoItemTypeFromString(type: String): InfoType {
|
||||
return when (type) {
|
||||
ID_STREAM -> InfoType.STREAM
|
||||
ID_PLAYLIST -> InfoType.PLAYLIST
|
||||
ID_CHANNEL -> InfoType.CHANNEL
|
||||
else -> throw IllegalStateException("Unexpected value: $type")
|
||||
}
|
||||
}
|
||||
|
||||
internal fun parseError(mediaId: String): ContentNotAvailableException {
|
||||
return ContentNotAvailableException("Failed to parse media ID $mediaId")
|
||||
}
|
@ -0,0 +1,399 @@
|
||||
package org.schabi.newpipe.player.mediabrowser
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.support.v4.media.MediaBrowserCompat
|
||||
import android.support.v4.media.MediaDescriptionCompat
|
||||
import android.util.Log
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.media.MediaBrowserServiceCompat
|
||||
import androidx.media.MediaBrowserServiceCompat.Result
|
||||
import androidx.media.utils.MediaConstants
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.schabi.newpipe.MainActivity.DEBUG
|
||||
import org.schabi.newpipe.NewPipeDatabase
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.database.history.model.StreamHistoryEntry
|
||||
import org.schabi.newpipe.database.playlist.PlaylistLocalItem
|
||||
import org.schabi.newpipe.database.playlist.PlaylistStreamEntry
|
||||
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity
|
||||
import org.schabi.newpipe.extractor.InfoItem
|
||||
import org.schabi.newpipe.extractor.InfoItem.InfoType
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem
|
||||
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException
|
||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem
|
||||
import org.schabi.newpipe.extractor.search.SearchInfo
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||
import org.schabi.newpipe.local.bookmark.MergedPlaylistManager
|
||||
import org.schabi.newpipe.local.playlist.LocalPlaylistManager
|
||||
import org.schabi.newpipe.local.playlist.RemotePlaylistManager
|
||||
import org.schabi.newpipe.util.ExtractorHelper
|
||||
import org.schabi.newpipe.util.ServiceHelper
|
||||
import org.schabi.newpipe.util.image.ImageStrategy
|
||||
import java.util.function.Consumer
|
||||
|
||||
/**
|
||||
* This class is used to cleanly separate the Service implementation (in
|
||||
* [org.schabi.newpipe.player.PlayerService]) and the media browser implementation (in this file).
|
||||
*
|
||||
* @param notifyChildrenChanged takes the parent id of the children that changed
|
||||
*/
|
||||
class MediaBrowserImpl(
|
||||
private val context: Context,
|
||||
notifyChildrenChanged: Consumer<String>, // parentId
|
||||
) {
|
||||
private val database = NewPipeDatabase.getInstance(context)
|
||||
private var disposables = CompositeDisposable()
|
||||
|
||||
init {
|
||||
// this will listen to changes in the bookmarks until this MediaBrowserImpl is dispose()d
|
||||
disposables.add(
|
||||
getMergedPlaylists().subscribe { notifyChildrenChanged.accept(ID_BOOKMARKS) }
|
||||
)
|
||||
}
|
||||
|
||||
//region Cleanup
|
||||
fun dispose() {
|
||||
disposables.dispose()
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region onGetRoot
|
||||
fun onGetRoot(
|
||||
clientPackageName: String,
|
||||
clientUid: Int,
|
||||
rootHints: Bundle?
|
||||
): MediaBrowserServiceCompat.BrowserRoot {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onGetRoot($clientPackageName, $clientUid, $rootHints)")
|
||||
}
|
||||
|
||||
val extras = Bundle()
|
||||
extras.putBoolean(
|
||||
MediaConstants.BROWSER_SERVICE_EXTRAS_KEY_SEARCH_SUPPORTED, true
|
||||
)
|
||||
return MediaBrowserServiceCompat.BrowserRoot(ID_ROOT, extras)
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region onLoadChildren
|
||||
fun onLoadChildren(parentId: String, result: Result<List<MediaBrowserCompat.MediaItem>>) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onLoadChildren($parentId)")
|
||||
}
|
||||
|
||||
result.detach() // allows sendResult() to happen later
|
||||
disposables.add(
|
||||
onLoadChildren(parentId)
|
||||
.subscribe(
|
||||
{ result.sendResult(it) },
|
||||
{ throwable ->
|
||||
// null indicates an error, see the docs of MediaSessionCompat.onSearch()
|
||||
result.sendResult(null)
|
||||
Log.e(TAG, "onLoadChildren error for parentId=$parentId: $throwable")
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun onLoadChildren(parentId: String): Single<List<MediaBrowserCompat.MediaItem>> {
|
||||
try {
|
||||
val parentIdUri = Uri.parse(parentId)
|
||||
val path = ArrayList(parentIdUri.pathSegments)
|
||||
|
||||
if (path.isEmpty()) {
|
||||
return Single.just(
|
||||
listOf(
|
||||
createRootMediaItem(
|
||||
ID_BOOKMARKS,
|
||||
context.resources.getString(R.string.tab_bookmarks_short),
|
||||
R.drawable.ic_bookmark_white
|
||||
),
|
||||
createRootMediaItem(
|
||||
ID_HISTORY,
|
||||
context.resources.getString(R.string.action_history),
|
||||
R.drawable.ic_history_white
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
when (/*val uriType = */path.removeAt(0)) {
|
||||
ID_BOOKMARKS -> {
|
||||
if (path.isEmpty()) {
|
||||
return populateBookmarks()
|
||||
}
|
||||
if (path.size == 2) {
|
||||
val localOrRemote = path[0]
|
||||
val playlistId = path[1].toLong()
|
||||
if (localOrRemote == ID_LOCAL) {
|
||||
return populateLocalPlaylist(playlistId)
|
||||
} else if (localOrRemote == ID_REMOTE) {
|
||||
return populateRemotePlaylist(playlistId)
|
||||
}
|
||||
}
|
||||
Log.w(TAG, "Unknown playlist URI: $parentId")
|
||||
throw parseError(parentId)
|
||||
}
|
||||
|
||||
ID_HISTORY -> return populateHistory()
|
||||
|
||||
else -> throw parseError(parentId)
|
||||
}
|
||||
} catch (e: ContentNotAvailableException) {
|
||||
return Single.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createRootMediaItem(
|
||||
mediaId: String?,
|
||||
folderName: String?,
|
||||
@DrawableRes iconResId: Int
|
||||
): MediaBrowserCompat.MediaItem {
|
||||
val builder = MediaDescriptionCompat.Builder()
|
||||
builder.setMediaId(mediaId)
|
||||
builder.setTitle(folderName)
|
||||
val resources = context.resources
|
||||
builder.setIconUri(
|
||||
Uri.Builder()
|
||||
.scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)
|
||||
.authority(resources.getResourcePackageName(iconResId))
|
||||
.appendPath(resources.getResourceTypeName(iconResId))
|
||||
.appendPath(resources.getResourceEntryName(iconResId))
|
||||
.build()
|
||||
)
|
||||
|
||||
val extras = Bundle()
|
||||
extras.putString(
|
||||
MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE,
|
||||
context.getString(R.string.app_name)
|
||||
)
|
||||
builder.setExtras(extras)
|
||||
return MediaBrowserCompat.MediaItem(
|
||||
builder.build(),
|
||||
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
|
||||
)
|
||||
}
|
||||
|
||||
private fun createPlaylistMediaItem(playlist: PlaylistLocalItem): MediaBrowserCompat.MediaItem {
|
||||
val builder = MediaDescriptionCompat.Builder()
|
||||
builder
|
||||
.setMediaId(createMediaIdForInfoItem(playlist is PlaylistRemoteEntity, playlist.uid))
|
||||
.setTitle(playlist.orderingName)
|
||||
.setIconUri(playlist.thumbnailUrl?.let { Uri.parse(it) })
|
||||
|
||||
val extras = Bundle()
|
||||
extras.putString(
|
||||
MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE,
|
||||
context.resources.getString(R.string.tab_bookmarks),
|
||||
)
|
||||
builder.setExtras(extras)
|
||||
return MediaBrowserCompat.MediaItem(
|
||||
builder.build(),
|
||||
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE,
|
||||
)
|
||||
}
|
||||
|
||||
private fun createInfoItemMediaItem(item: InfoItem): MediaBrowserCompat.MediaItem? {
|
||||
val builder = MediaDescriptionCompat.Builder()
|
||||
builder.setMediaId(createMediaIdForInfoItem(item))
|
||||
.setTitle(item.name)
|
||||
|
||||
when (item.infoType) {
|
||||
InfoType.STREAM -> builder.setSubtitle((item as StreamInfoItem).uploaderName)
|
||||
InfoType.PLAYLIST -> builder.setSubtitle((item as PlaylistInfoItem).uploaderName)
|
||||
InfoType.CHANNEL -> builder.setSubtitle((item as ChannelInfoItem).description)
|
||||
else -> return null
|
||||
}
|
||||
|
||||
ImageStrategy.choosePreferredImage(item.thumbnails)?.let {
|
||||
builder.setIconUri(Uri.parse(it))
|
||||
}
|
||||
|
||||
return MediaBrowserCompat.MediaItem(
|
||||
builder.build(),
|
||||
MediaBrowserCompat.MediaItem.FLAG_PLAYABLE
|
||||
)
|
||||
}
|
||||
|
||||
private fun buildMediaId(): Uri.Builder {
|
||||
return Uri.Builder().authority(ID_AUTHORITY)
|
||||
}
|
||||
|
||||
private fun buildPlaylistMediaId(playlistType: String?): Uri.Builder {
|
||||
return buildMediaId()
|
||||
.appendPath(ID_BOOKMARKS)
|
||||
.appendPath(playlistType)
|
||||
}
|
||||
|
||||
private fun buildLocalPlaylistItemMediaId(isRemote: Boolean, playlistId: Long): Uri.Builder {
|
||||
return buildPlaylistMediaId(if (isRemote) ID_REMOTE else ID_LOCAL)
|
||||
.appendPath(playlistId.toString())
|
||||
}
|
||||
|
||||
private fun buildInfoItemMediaId(item: InfoItem): Uri.Builder {
|
||||
return buildMediaId()
|
||||
.appendPath(ID_INFO_ITEM)
|
||||
.appendPath(infoItemTypeToString(item.infoType))
|
||||
.appendPath(item.serviceId.toString())
|
||||
.appendQueryParameter(ID_URL, item.url)
|
||||
}
|
||||
|
||||
private fun createMediaIdForInfoItem(isRemote: Boolean, playlistId: Long): String {
|
||||
return buildLocalPlaylistItemMediaId(isRemote, playlistId)
|
||||
.build().toString()
|
||||
}
|
||||
|
||||
private fun createLocalPlaylistStreamMediaItem(
|
||||
playlistId: Long,
|
||||
item: PlaylistStreamEntry,
|
||||
index: Int,
|
||||
): MediaBrowserCompat.MediaItem {
|
||||
val builder = MediaDescriptionCompat.Builder()
|
||||
builder.setMediaId(createMediaIdForPlaylistIndex(false, playlistId, index))
|
||||
.setTitle(item.streamEntity.title)
|
||||
.setSubtitle(item.streamEntity.uploader)
|
||||
.setIconUri(Uri.parse(item.streamEntity.thumbnailUrl))
|
||||
|
||||
return MediaBrowserCompat.MediaItem(
|
||||
builder.build(),
|
||||
MediaBrowserCompat.MediaItem.FLAG_PLAYABLE
|
||||
)
|
||||
}
|
||||
|
||||
private fun createRemotePlaylistStreamMediaItem(
|
||||
playlistId: Long,
|
||||
item: StreamInfoItem,
|
||||
index: Int,
|
||||
): MediaBrowserCompat.MediaItem {
|
||||
val builder = MediaDescriptionCompat.Builder()
|
||||
builder.setMediaId(createMediaIdForPlaylistIndex(true, playlistId, index))
|
||||
.setTitle(item.name)
|
||||
.setSubtitle(item.uploaderName)
|
||||
|
||||
ImageStrategy.choosePreferredImage(item.thumbnails)?.let {
|
||||
builder.setIconUri(Uri.parse(it))
|
||||
}
|
||||
|
||||
return MediaBrowserCompat.MediaItem(
|
||||
builder.build(),
|
||||
MediaBrowserCompat.MediaItem.FLAG_PLAYABLE
|
||||
)
|
||||
}
|
||||
|
||||
private fun createMediaIdForPlaylistIndex(
|
||||
isRemote: Boolean,
|
||||
playlistId: Long,
|
||||
index: Int,
|
||||
): String {
|
||||
return buildLocalPlaylistItemMediaId(isRemote, playlistId)
|
||||
.appendPath(index.toString())
|
||||
.build().toString()
|
||||
}
|
||||
|
||||
private fun createMediaIdForInfoItem(item: InfoItem): String {
|
||||
return buildInfoItemMediaId(item).build().toString()
|
||||
}
|
||||
|
||||
private fun populateHistory(): Single<List<MediaBrowserCompat.MediaItem>> {
|
||||
val history = database.streamHistoryDAO().getHistory().firstOrError()
|
||||
return history.map { items ->
|
||||
items.map { this.createHistoryMediaItem(it) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun createHistoryMediaItem(streamHistoryEntry: StreamHistoryEntry): MediaBrowserCompat.MediaItem {
|
||||
val builder = MediaDescriptionCompat.Builder()
|
||||
val mediaId = buildMediaId()
|
||||
.appendPath(ID_HISTORY)
|
||||
.appendPath(streamHistoryEntry.streamId.toString())
|
||||
.build().toString()
|
||||
builder.setMediaId(mediaId)
|
||||
.setTitle(streamHistoryEntry.streamEntity.title)
|
||||
.setSubtitle(streamHistoryEntry.streamEntity.uploader)
|
||||
.setIconUri(Uri.parse(streamHistoryEntry.streamEntity.thumbnailUrl))
|
||||
|
||||
return MediaBrowserCompat.MediaItem(
|
||||
builder.build(),
|
||||
MediaBrowserCompat.MediaItem.FLAG_PLAYABLE
|
||||
)
|
||||
}
|
||||
|
||||
private fun getMergedPlaylists(): Flowable<MutableList<PlaylistLocalItem>> {
|
||||
return MergedPlaylistManager.getMergedOrderedPlaylists(
|
||||
LocalPlaylistManager(database),
|
||||
RemotePlaylistManager(database)
|
||||
)
|
||||
}
|
||||
|
||||
private fun populateBookmarks(): Single<List<MediaBrowserCompat.MediaItem>> {
|
||||
val playlists = getMergedPlaylists().firstOrError()
|
||||
return playlists.map { playlist ->
|
||||
playlist.map { this.createPlaylistMediaItem(it) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun populateLocalPlaylist(playlistId: Long): Single<List<MediaBrowserCompat.MediaItem>> {
|
||||
val playlist = LocalPlaylistManager(database).getPlaylistStreams(playlistId).firstOrError()
|
||||
return playlist.map { items ->
|
||||
items.mapIndexed { index, item ->
|
||||
createLocalPlaylistStreamMediaItem(playlistId, item, index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun populateRemotePlaylist(playlistId: Long): Single<List<MediaBrowserCompat.MediaItem>> {
|
||||
return RemotePlaylistManager(database).getPlaylist(playlistId).firstOrError()
|
||||
.flatMap { ExtractorHelper.getPlaylistInfo(it.serviceId, it.url, false) }
|
||||
.map {
|
||||
// ignore it.errors, i.e. ignore errors about specific items, since there would
|
||||
// be no way to show the error properly in Android Auto anyway
|
||||
it.relatedItems.mapIndexed { index, item ->
|
||||
createRemotePlaylistStreamMediaItem(playlistId, item, index)
|
||||
}
|
||||
}
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region Search
|
||||
fun onSearch(
|
||||
query: String,
|
||||
result: Result<List<MediaBrowserCompat.MediaItem>>
|
||||
) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onSearch($query)")
|
||||
}
|
||||
|
||||
result.detach() // allows sendResult() to happen later
|
||||
disposables.add(
|
||||
searchMusicBySongTitle(query)
|
||||
// ignore it.errors, i.e. ignore errors about specific items, since there would
|
||||
// be no way to show the error properly in Android Auto anyway
|
||||
.map { it.relatedItems.mapNotNull(this::createInfoItemMediaItem) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe(
|
||||
{ result.sendResult(it) },
|
||||
{ throwable ->
|
||||
// null indicates an error, see the docs of MediaSessionCompat.onSearch()
|
||||
result.sendResult(null)
|
||||
Log.e(TAG, "Search error for query=\"$query\": $throwable")
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun searchMusicBySongTitle(query: String?): Single<SearchInfo> {
|
||||
val serviceId = ServiceHelper.getSelectedServiceId(context)
|
||||
return ExtractorHelper.searchFor(serviceId, query, listOf(), "")
|
||||
}
|
||||
//endregion
|
||||
|
||||
companion object {
|
||||
private val TAG: String = MediaBrowserImpl::class.java.getSimpleName()
|
||||
}
|
||||
}
|
@ -0,0 +1,259 @@
|
||||
package org.schabi.newpipe.player.mediabrowser
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.ResultReceiver
|
||||
import android.support.v4.media.session.PlaybackStateCompat
|
||||
import android.util.Log
|
||||
import com.google.android.exoplayer2.Player
|
||||
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector.PlaybackPreparer
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.schabi.newpipe.MainActivity
|
||||
import org.schabi.newpipe.NewPipeDatabase
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.extractor.InfoItem.InfoType
|
||||
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException
|
||||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler
|
||||
import org.schabi.newpipe.local.playlist.LocalPlaylistManager
|
||||
import org.schabi.newpipe.local.playlist.RemotePlaylistManager
|
||||
import org.schabi.newpipe.player.playqueue.ChannelTabPlayQueue
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue
|
||||
import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue
|
||||
import org.schabi.newpipe.player.playqueue.SinglePlayQueue
|
||||
import org.schabi.newpipe.util.ChannelTabHelper
|
||||
import org.schabi.newpipe.util.ExtractorHelper
|
||||
import org.schabi.newpipe.util.NavigationHelper
|
||||
import java.util.function.BiConsumer
|
||||
import java.util.function.Consumer
|
||||
|
||||
/**
|
||||
* This class is used to cleanly separate the Service implementation (in
|
||||
* [org.schabi.newpipe.player.PlayerService]) and the playback preparer implementation (in this
|
||||
* file). We currently use the playback preparer only in conjunction with the media browser: the
|
||||
* playback preparer will receive the media URLs generated by [MediaBrowserImpl] and will start
|
||||
* playback of the corresponding streams or playlists.
|
||||
*
|
||||
* @param setMediaSessionError takes an error String and an error code from [PlaybackStateCompat],
|
||||
* calls `sessionConnector.setCustomErrorMessage(errorString, errorCode)`
|
||||
* @param clearMediaSessionError calls `sessionConnector.setCustomErrorMessage(null)`
|
||||
* @param onPrepare takes playWhenReady, calls `player.prepare()`; this is needed because
|
||||
* `MediaSessionConnector`'s `onPlay()` method calls this class' [onPrepare] instead of
|
||||
* `player.prepare()` if the playback preparer is not null, but we want the original behavior
|
||||
*/
|
||||
class MediaBrowserPlaybackPreparer(
|
||||
private val context: Context,
|
||||
private val setMediaSessionError: BiConsumer<String, Int>, // error string, error code
|
||||
private val clearMediaSessionError: Runnable,
|
||||
private val onPrepare: Consumer<Boolean>,
|
||||
) : PlaybackPreparer {
|
||||
private val database = NewPipeDatabase.getInstance(context)
|
||||
private var disposable: Disposable? = null
|
||||
|
||||
fun dispose() {
|
||||
disposable?.dispose()
|
||||
}
|
||||
|
||||
//region Overrides
|
||||
override fun getSupportedPrepareActions(): Long {
|
||||
return PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID
|
||||
}
|
||||
|
||||
override fun onPrepare(playWhenReady: Boolean) {
|
||||
onPrepare.accept(playWhenReady)
|
||||
}
|
||||
|
||||
override fun onPrepareFromMediaId(mediaId: String, playWhenReady: Boolean, extras: Bundle?) {
|
||||
if (MainActivity.DEBUG) {
|
||||
Log.d(TAG, "onPrepareFromMediaId($mediaId, $playWhenReady, $extras)")
|
||||
}
|
||||
|
||||
disposable?.dispose()
|
||||
disposable = extractPlayQueueFromMediaId(mediaId)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ playQueue ->
|
||||
clearMediaSessionError.run()
|
||||
NavigationHelper.playOnBackgroundPlayer(context, playQueue, playWhenReady)
|
||||
},
|
||||
{ throwable ->
|
||||
Log.e(TAG, "Failed to start playback of media ID [$mediaId]", throwable)
|
||||
onPrepareError()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun onPrepareFromSearch(query: String, playWhenReady: Boolean, extras: Bundle?) {
|
||||
onUnsupportedError()
|
||||
}
|
||||
|
||||
override fun onPrepareFromUri(uri: Uri, playWhenReady: Boolean, extras: Bundle?) {
|
||||
onUnsupportedError()
|
||||
}
|
||||
|
||||
override fun onCommand(
|
||||
player: Player,
|
||||
command: String,
|
||||
extras: Bundle?,
|
||||
cb: ResultReceiver?
|
||||
): Boolean {
|
||||
return false
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region Errors
|
||||
private fun onUnsupportedError() {
|
||||
setMediaSessionError.accept(
|
||||
context.getString(R.string.content_not_supported),
|
||||
PlaybackStateCompat.ERROR_CODE_NOT_SUPPORTED
|
||||
)
|
||||
}
|
||||
|
||||
private fun onPrepareError() {
|
||||
setMediaSessionError.accept(
|
||||
context.getString(R.string.error_snackbar_message),
|
||||
PlaybackStateCompat.ERROR_CODE_APP_ERROR
|
||||
)
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region Building play queues from playlists and history
|
||||
private fun extractLocalPlayQueue(playlistId: Long, index: Int): Single<PlayQueue> {
|
||||
return LocalPlaylistManager(database).getPlaylistStreams(playlistId).firstOrError()
|
||||
.map { items -> SinglePlayQueue(items.map { it.toStreamInfoItem() }, index) }
|
||||
}
|
||||
|
||||
private fun extractRemotePlayQueue(playlistId: Long, index: Int): Single<PlayQueue> {
|
||||
return RemotePlaylistManager(database).getPlaylist(playlistId).firstOrError()
|
||||
.flatMap { ExtractorHelper.getPlaylistInfo(it.serviceId, it.url, false) }
|
||||
// ignore info.errors, i.e. ignore errors about specific items, since there would
|
||||
// be no way to show the error properly in Android Auto anyway
|
||||
.map { info -> PlaylistPlayQueue(info, index) }
|
||||
}
|
||||
|
||||
private fun extractPlayQueueFromMediaId(mediaId: String): Single<PlayQueue> {
|
||||
try {
|
||||
val mediaIdUri = Uri.parse(mediaId)
|
||||
val path = ArrayList(mediaIdUri.pathSegments)
|
||||
if (path.isEmpty()) {
|
||||
throw parseError(mediaId)
|
||||
}
|
||||
|
||||
return when (/*val uriType = */path.removeAt(0)) {
|
||||
ID_BOOKMARKS -> extractPlayQueueFromPlaylistMediaId(
|
||||
mediaId,
|
||||
path,
|
||||
mediaIdUri.getQueryParameter(ID_URL)
|
||||
)
|
||||
|
||||
ID_HISTORY -> extractPlayQueueFromHistoryMediaId(mediaId, path)
|
||||
|
||||
ID_INFO_ITEM -> extractPlayQueueFromInfoItemMediaId(
|
||||
mediaId,
|
||||
path,
|
||||
mediaIdUri.getQueryParameter(ID_URL) ?: throw parseError(mediaId)
|
||||
)
|
||||
|
||||
else -> throw parseError(mediaId)
|
||||
}
|
||||
} catch (e: ContentNotAvailableException) {
|
||||
return Single.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(ContentNotAvailableException::class)
|
||||
private fun extractPlayQueueFromPlaylistMediaId(
|
||||
mediaId: String,
|
||||
path: MutableList<String>,
|
||||
url: String?,
|
||||
): Single<PlayQueue> {
|
||||
if (path.isEmpty()) {
|
||||
throw parseError(mediaId)
|
||||
}
|
||||
|
||||
when (val playlistType = path.removeAt(0)) {
|
||||
ID_LOCAL, ID_REMOTE -> {
|
||||
if (path.size != 2) {
|
||||
throw parseError(mediaId)
|
||||
}
|
||||
val playlistId = path[0].toLong()
|
||||
val index = path[1].toInt()
|
||||
return if (playlistType == ID_LOCAL)
|
||||
extractLocalPlayQueue(playlistId, index)
|
||||
else
|
||||
extractRemotePlayQueue(playlistId, index)
|
||||
}
|
||||
|
||||
ID_URL -> {
|
||||
if (path.size != 1 || url == null) {
|
||||
throw parseError(mediaId)
|
||||
}
|
||||
|
||||
val serviceId = path[0].toInt()
|
||||
return ExtractorHelper.getPlaylistInfo(serviceId, url, false)
|
||||
.map { PlaylistPlayQueue(it) }
|
||||
}
|
||||
|
||||
else -> throw parseError(mediaId)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(ContentNotAvailableException::class)
|
||||
private fun extractPlayQueueFromHistoryMediaId(
|
||||
mediaId: String,
|
||||
path: List<String>,
|
||||
): Single<PlayQueue> {
|
||||
if (path.size != 1) {
|
||||
throw parseError(mediaId)
|
||||
}
|
||||
|
||||
val streamId = path[0].toLong()
|
||||
return database.streamHistoryDAO().getHistory()
|
||||
.firstOrError()
|
||||
.map { items ->
|
||||
val infoItems = items
|
||||
.filter { it.streamId == streamId }
|
||||
.map { it.toStreamInfoItem() }
|
||||
SinglePlayQueue(infoItems, 0)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(ContentNotAvailableException::class)
|
||||
private fun extractPlayQueueFromInfoItemMediaId(
|
||||
mediaId: String,
|
||||
path: List<String>,
|
||||
url: String,
|
||||
): Single<PlayQueue> {
|
||||
if (path.size != 2) {
|
||||
throw parseError(mediaId)
|
||||
}
|
||||
|
||||
val serviceId = path[1].toInt()
|
||||
return when (/*val infoItemType = */infoItemTypeFromString(path[0])) {
|
||||
InfoType.STREAM -> ExtractorHelper.getStreamInfo(serviceId, url, false)
|
||||
.map { SinglePlayQueue(it) }
|
||||
|
||||
InfoType.PLAYLIST -> ExtractorHelper.getPlaylistInfo(serviceId, url, false)
|
||||
.map { PlaylistPlayQueue(it) }
|
||||
|
||||
InfoType.CHANNEL -> ExtractorHelper.getChannelInfo(serviceId, url, false)
|
||||
.map { info ->
|
||||
val playableTab = info.tabs
|
||||
.firstOrNull { ChannelTabHelper.isStreamsTab(it) }
|
||||
?: throw ContentNotAvailableException("No streams tab found")
|
||||
return@map ChannelTabPlayQueue(serviceId, ListLinkHandler(playableTab))
|
||||
}
|
||||
|
||||
else -> throw parseError(mediaId)
|
||||
}
|
||||
}
|
||||
//endregion
|
||||
|
||||
companion object {
|
||||
private val TAG = MediaBrowserPlaybackPreparer::class.simpleName
|
||||
}
|
||||
}
|
@ -38,10 +38,10 @@ public class MediaSessionPlayerUi extends PlayerUi
|
||||
implements SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
private static final String TAG = "MediaSessUi";
|
||||
|
||||
@Nullable
|
||||
private MediaSessionCompat mediaSession;
|
||||
@Nullable
|
||||
private MediaSessionConnector sessionConnector;
|
||||
@NonNull
|
||||
private final MediaSessionCompat mediaSession;
|
||||
@NonNull
|
||||
private final MediaSessionConnector sessionConnector;
|
||||
|
||||
private final String ignoreHardwareMediaButtonsKey;
|
||||
private boolean shouldIgnoreHardwareMediaButtons = false;
|
||||
@ -50,9 +50,13 @@ public class MediaSessionPlayerUi extends PlayerUi
|
||||
private List<NotificationActionData> prevNotificationActions = List.of();
|
||||
|
||||
|
||||
public MediaSessionPlayerUi(@NonNull final Player player) {
|
||||
public MediaSessionPlayerUi(@NonNull final Player player,
|
||||
@NonNull final MediaSessionCompat mediaSession,
|
||||
@NonNull final MediaSessionConnector sessionConnector) {
|
||||
super(player);
|
||||
ignoreHardwareMediaButtonsKey =
|
||||
this.mediaSession = mediaSession;
|
||||
this.sessionConnector = sessionConnector;
|
||||
this.ignoreHardwareMediaButtonsKey =
|
||||
context.getString(R.string.ignore_hardware_media_buttons_key);
|
||||
}
|
||||
|
||||
@ -61,10 +65,8 @@ public class MediaSessionPlayerUi extends PlayerUi
|
||||
super.initPlayer();
|
||||
destroyPlayer(); // release previously used resources
|
||||
|
||||
mediaSession = new MediaSessionCompat(context, TAG);
|
||||
mediaSession.setActive(true);
|
||||
|
||||
sessionConnector = new MediaSessionConnector(mediaSession);
|
||||
sessionConnector.setQueueNavigator(new PlayQueueNavigator(mediaSession, player));
|
||||
sessionConnector.setPlayer(getForwardingPlayer());
|
||||
|
||||
@ -89,27 +91,18 @@ public class MediaSessionPlayerUi extends PlayerUi
|
||||
public void destroyPlayer() {
|
||||
super.destroyPlayer();
|
||||
player.getPrefs().unregisterOnSharedPreferenceChangeListener(this);
|
||||
if (sessionConnector != null) {
|
||||
sessionConnector.setMediaButtonEventHandler(null);
|
||||
sessionConnector.setPlayer(null);
|
||||
sessionConnector.setQueueNavigator(null);
|
||||
sessionConnector = null;
|
||||
}
|
||||
if (mediaSession != null) {
|
||||
mediaSession.setActive(false);
|
||||
mediaSession.release();
|
||||
mediaSession = null;
|
||||
}
|
||||
sessionConnector.setMediaButtonEventHandler(null);
|
||||
sessionConnector.setPlayer(null);
|
||||
sessionConnector.setQueueNavigator(null);
|
||||
mediaSession.setActive(false);
|
||||
prevNotificationActions = List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onThumbnailLoaded(@Nullable final Bitmap bitmap) {
|
||||
super.onThumbnailLoaded(bitmap);
|
||||
if (sessionConnector != null) {
|
||||
// the thumbnail is now loaded: invalidate the metadata to trigger a metadata update
|
||||
sessionConnector.invalidateMediaSessionMetadata();
|
||||
}
|
||||
// the thumbnail is now loaded: invalidate the metadata to trigger a metadata update
|
||||
sessionConnector.invalidateMediaSessionMetadata();
|
||||
}
|
||||
|
||||
|
||||
@ -200,8 +193,8 @@ public class MediaSessionPlayerUi extends PlayerUi
|
||||
return;
|
||||
}
|
||||
|
||||
if (sessionConnector == null) {
|
||||
// sessionConnector will be null after destroyPlayer is called
|
||||
if (!mediaSession.isActive()) {
|
||||
// mediaSession will be inactive after destroyPlayer is called
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -28,13 +28,17 @@ abstract class AbstractInfoPlayQueue<T extends ListInfo<? extends InfoItem>>
|
||||
private transient Disposable fetchReactor;
|
||||
|
||||
protected AbstractInfoPlayQueue(final T info) {
|
||||
this(info, 0);
|
||||
}
|
||||
|
||||
protected AbstractInfoPlayQueue(final T info, final int index) {
|
||||
this(info.getServiceId(), info.getUrl(), info.getNextPage(),
|
||||
info.getRelatedItems()
|
||||
.stream()
|
||||
.filter(StreamInfoItem.class::isInstance)
|
||||
.map(StreamInfoItem.class::cast)
|
||||
.collect(Collectors.toList()),
|
||||
0);
|
||||
index);
|
||||
}
|
||||
|
||||
protected AbstractInfoPlayQueue(final int serviceId,
|
||||
|
@ -16,6 +16,10 @@ public final class PlaylistPlayQueue extends AbstractInfoPlayQueue<PlaylistInfo>
|
||||
super(info);
|
||||
}
|
||||
|
||||
public PlaylistPlayQueue(final PlaylistInfo info, final int index) {
|
||||
super(info, index);
|
||||
}
|
||||
|
||||
public PlaylistPlayQueue(final int serviceId,
|
||||
final String url,
|
||||
final Page nextPage,
|
||||
|
@ -25,9 +25,7 @@ import android.graphics.Color;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.provider.Settings;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
import android.util.TypedValue;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@ -44,6 +42,7 @@ import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.google.android.exoplayer2.ui.SubtitleView;
|
||||
import com.google.android.exoplayer2.video.VideoSize;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
@ -522,11 +521,8 @@ public final class MainPlayerUi extends VideoPlayerUi implements View.OnLayoutCh
|
||||
|
||||
@Override
|
||||
protected void setupSubtitleView(final float captionScale) {
|
||||
final DisplayMetrics metrics = context.getResources().getDisplayMetrics();
|
||||
final int minimumLength = Math.min(metrics.heightPixels, metrics.widthPixels);
|
||||
final float captionRatioInverse = 20f + 4f * (1.0f - captionScale);
|
||||
binding.subtitleView.setFixedTextSize(
|
||||
TypedValue.COMPLEX_UNIT_PX, minimumLength / captionRatioInverse);
|
||||
binding.subtitleView.setFractionalTextSize(
|
||||
SubtitleView.DEFAULT_TEXT_SIZE_FRACTION * captionScale);
|
||||
}
|
||||
//endregion
|
||||
|
||||
|
@ -382,7 +382,7 @@ public final class PopupPlayerUi extends VideoPlayerUi {
|
||||
private void end() {
|
||||
windowManager.removeView(closeOverlayBinding.getRoot());
|
||||
closeOverlayBinding = null;
|
||||
player.getService().stopService();
|
||||
player.getService().destroyPlayerAndStopService();
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
@ -424,9 +424,8 @@ public final class PopupPlayerUi extends VideoPlayerUi {
|
||||
|
||||
@Override
|
||||
protected void setupSubtitleView(final float captionScale) {
|
||||
final float captionRatio = (captionScale - 1.0f) / 5.0f + 1.0f;
|
||||
binding.subtitleView.setFractionalTextSize(
|
||||
SubtitleView.DEFAULT_TEXT_SIZE_FRACTION * captionRatio);
|
||||
SubtitleView.DEFAULT_TEXT_SIZE_FRACTION * captionScale);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -1414,6 +1414,10 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa
|
||||
binding.subtitleView.setStyle(captionStyle);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param captionScale Value returned by {@link PlayerHelper#getCaptionScale}.
|
||||
*/
|
||||
protected abstract void setupSubtitleView(float captionScale);
|
||||
//endregion
|
||||
|
||||
|
@ -1,10 +1,15 @@
|
||||
package org.schabi.newpipe.settings;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.provider.Settings;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.appcompat.app.AppCompatDelegate;
|
||||
import androidx.preference.Preference;
|
||||
|
||||
import org.schabi.newpipe.DownloaderImpl;
|
||||
@ -17,12 +22,11 @@ import org.schabi.newpipe.util.image.PicassoHelper;
|
||||
import org.schabi.newpipe.util.image.PreferredImageQuality;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Locale;
|
||||
|
||||
public class ContentSettingsFragment extends BasePreferenceFragment {
|
||||
private String youtubeRestrictedModeEnabledKey;
|
||||
|
||||
private Localization initialSelectedLocalization;
|
||||
private ContentCountry initialSelectedContentCountry;
|
||||
private String initialLanguage;
|
||||
|
||||
@Override
|
||||
@ -31,12 +35,28 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
|
||||
|
||||
addPreferencesFromResourceRegistry();
|
||||
|
||||
initialSelectedLocalization = org.schabi.newpipe.util.Localization
|
||||
.getPreferredLocalization(requireContext());
|
||||
initialSelectedContentCountry = org.schabi.newpipe.util.Localization
|
||||
.getPreferredContentCountry(requireContext());
|
||||
initialLanguage = defaultPreferences.getString(getString(R.string.app_language_key), "en");
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 33) {
|
||||
requirePreference(R.string.app_language_key).setVisible(false);
|
||||
final Preference newAppLanguagePref =
|
||||
requirePreference(R.string.app_language_android_13_and_up_key);
|
||||
newAppLanguagePref.setSummaryProvider(preference -> {
|
||||
final Locale customLocale = AppCompatDelegate.getApplicationLocales().get(0);
|
||||
if (customLocale != null) {
|
||||
return customLocale.getDisplayName();
|
||||
}
|
||||
return getString(R.string.systems_language);
|
||||
});
|
||||
newAppLanguagePref.setOnPreferenceClickListener(preference -> {
|
||||
final Intent intent = new Intent(Settings.ACTION_APP_LOCALE_SETTINGS)
|
||||
.setData(Uri.fromParts("package", requireContext().getPackageName(), null));
|
||||
startActivity(intent);
|
||||
return true;
|
||||
});
|
||||
newAppLanguagePref.setVisible(true);
|
||||
}
|
||||
|
||||
final Preference imageQualityPreference = requirePreference(R.string.image_quality_key);
|
||||
imageQualityPreference.setOnPreferenceChangeListener(
|
||||
(preference, newValue) -> {
|
||||
@ -72,19 +92,21 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
|
||||
final Localization selectedLocalization = org.schabi.newpipe.util.Localization
|
||||
.getPreferredLocalization(requireContext());
|
||||
final ContentCountry selectedContentCountry = org.schabi.newpipe.util.Localization
|
||||
.getPreferredContentCountry(requireContext());
|
||||
final String selectedLanguage =
|
||||
defaultPreferences.getString(getString(R.string.app_language_key), "en");
|
||||
|
||||
if (!selectedLocalization.equals(initialSelectedLocalization)
|
||||
|| !selectedContentCountry.equals(initialSelectedContentCountry)
|
||||
|| !selectedLanguage.equals(initialLanguage)) {
|
||||
Toast.makeText(requireContext(), R.string.localization_changes_requires_app_restart,
|
||||
Toast.LENGTH_LONG).show();
|
||||
|
||||
if (!selectedLanguage.equals(initialLanguage)) {
|
||||
if (Build.VERSION.SDK_INT < 33) {
|
||||
Toast.makeText(
|
||||
requireContext(),
|
||||
R.string.localization_changes_requires_app_restart,
|
||||
Toast.LENGTH_LONG
|
||||
).show();
|
||||
}
|
||||
final Localization selectedLocalization = org.schabi.newpipe.util.Localization
|
||||
.getPreferredLocalization(requireContext());
|
||||
final ContentCountry selectedContentCountry = org.schabi.newpipe.util.Localization
|
||||
.getPreferredContentCountry(requireContext());
|
||||
NewPipe.setupLocalization(selectedLocalization, selectedContentCountry);
|
||||
}
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ import android.view.InputDevice;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.WindowInsets;
|
||||
import android.view.WindowManager;
|
||||
import android.webkit.CookieManager;
|
||||
|
||||
import androidx.annotation.Dimension;
|
||||
import androidx.annotation.NonNull;
|
||||
@ -335,4 +336,17 @@ public final class DeviceUtils {
|
||||
&& !TX_50JXW834
|
||||
&& !HMB9213NW;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return whether the device has support for WebView, see
|
||||
* <a href="https://stackoverflow.com/a/69626735">https://stackoverflow.com/a/69626735</a>
|
||||
*/
|
||||
public static boolean supportsWebView() {
|
||||
try {
|
||||
CookieManager.getInstance();
|
||||
return true;
|
||||
} catch (final Throwable ignored) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -12,12 +12,15 @@ import android.os.Build;
|
||||
import android.text.TextUtils;
|
||||
import android.text.format.DateUtils;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.PluralsRes;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.appcompat.app.AppCompatDelegate;
|
||||
import androidx.core.math.MathUtils;
|
||||
import androidx.core.os.LocaleListCompat;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import org.ocpsoft.prettytime.PrettyTime;
|
||||
@ -39,6 +42,7 @@ import java.time.format.FormatStyle;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
|
||||
@ -63,6 +67,7 @@ import java.util.stream.Collectors;
|
||||
*/
|
||||
|
||||
public final class Localization {
|
||||
private static final String TAG = Localization.class.toString();
|
||||
public static final String DOT_SEPARATOR = " • ";
|
||||
private static PrettyTime prettyTime;
|
||||
|
||||
@ -101,6 +106,10 @@ public final class Localization {
|
||||
}
|
||||
|
||||
public static Locale getAppLocale(@NonNull final Context context) {
|
||||
if (Build.VERSION.SDK_INT >= 33) {
|
||||
final Locale customLocale = AppCompatDelegate.getApplicationLocales().get(0);
|
||||
return Objects.requireNonNullElseGet(customLocale, Locale::getDefault);
|
||||
}
|
||||
return getLocaleFromPrefs(context, R.string.app_language_key);
|
||||
}
|
||||
|
||||
@ -308,7 +317,7 @@ public final class Localization {
|
||||
* <ul>
|
||||
* <li>English (original)</li>
|
||||
* <li>English (descriptive)</li>
|
||||
* <li>Spanish (dubbed)</li>
|
||||
* <li>Spanish (Spain) (dubbed)</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param context the context used to get the app language
|
||||
@ -318,7 +327,7 @@ public final class Localization {
|
||||
public static String audioTrackName(@NonNull final Context context, final AudioStream track) {
|
||||
final String name;
|
||||
if (track.getAudioLocale() != null) {
|
||||
name = track.getAudioLocale().getDisplayLanguage(getAppLocale(context));
|
||||
name = track.getAudioLocale().getDisplayName();
|
||||
} else if (track.getAudioTrackName() != null) {
|
||||
name = track.getAudioTrackName();
|
||||
} else {
|
||||
@ -427,4 +436,32 @@ public final class Localization {
|
||||
final int safeCount = (int) MathUtils.clamp(count, Integer.MIN_VALUE, Integer.MAX_VALUE);
|
||||
return context.getResources().getQuantityString(pluralId, safeCount, formattedCount);
|
||||
}
|
||||
|
||||
public static void migrateAppLanguageSettingIfNecessary(@NonNull final Context context) {
|
||||
// Starting with pull request #12093, NewPipe on Android 13+ exclusively uses Android's
|
||||
// public per-app language APIs to read and set the UI language for NewPipe.
|
||||
// If running on Android 13+, the following code will migrate any existing custom
|
||||
// app language in SharedPreferences to use the public per-app language APIs instead.
|
||||
if (Build.VERSION.SDK_INT >= 33) {
|
||||
final SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
final String appLanguageKey = context.getString(R.string.app_language_key);
|
||||
final String appLanguageValue = sp.getString(appLanguageKey, null);
|
||||
if (appLanguageValue != null) {
|
||||
sp.edit().remove(appLanguageKey).apply();
|
||||
final String appLanguageDefaultValue =
|
||||
context.getString(R.string.default_localization_key);
|
||||
if (!appLanguageValue.equals(appLanguageDefaultValue)) {
|
||||
try {
|
||||
AppCompatDelegate.setApplicationLocales(
|
||||
LocaleListCompat.forLanguageTags(appLanguageValue)
|
||||
);
|
||||
} catch (final RuntimeException e) {
|
||||
Log.e(TAG, "Failed to migrate previous custom app language "
|
||||
+ "setting to public per-app language APIs"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -96,6 +96,7 @@ public final class NavigationHelper {
|
||||
}
|
||||
intent.putExtra(Player.PLAYER_TYPE, PlayerType.MAIN.valueForIntent());
|
||||
intent.putExtra(Player.RESUME_PLAYBACK, resumePlayback);
|
||||
intent.putExtra(PlayerService.SHOULD_START_FOREGROUND_EXTRA, true);
|
||||
|
||||
return intent;
|
||||
}
|
||||
@ -452,8 +453,12 @@ public final class NavigationHelper {
|
||||
if (fragment instanceof VideoDetailFragment && fragment.isVisible()) {
|
||||
onVideoDetailFragmentReady.run((VideoDetailFragment) fragment);
|
||||
} else {
|
||||
// Specify no url here, otherwise the VideoDetailFragment will start loading the
|
||||
// stream automatically if it's the first time it is being opened, but then
|
||||
// onVideoDetailFragmentReady will kick in and start another loading process.
|
||||
// See VideoDetailFragment.wasCleared() and its usage in doInitialLoadLogic().
|
||||
final VideoDetailFragment instance = VideoDetailFragment
|
||||
.getInstance(serviceId, url, title, playQueue);
|
||||
.getInstance(serviceId, null, title, playQueue);
|
||||
instance.setAutoPlay(autoPlay);
|
||||
|
||||
defaultTransaction(fragmentManager)
|
||||
|
@ -0,0 +1,113 @@
|
||||
package org.schabi.newpipe.util.potoken
|
||||
|
||||
import com.grack.nanojson.JsonObject
|
||||
import com.grack.nanojson.JsonParser
|
||||
import com.grack.nanojson.JsonWriter
|
||||
import okio.ByteString.Companion.decodeBase64
|
||||
import okio.ByteString.Companion.toByteString
|
||||
|
||||
/**
|
||||
* Parses the raw challenge data obtained from the Create endpoint and returns an object that can be
|
||||
* embedded in a JavaScript snippet.
|
||||
*/
|
||||
fun parseChallengeData(rawChallengeData: String): String {
|
||||
val scrambled = JsonParser.array().from(rawChallengeData)
|
||||
|
||||
val challengeData = if (scrambled.size > 1 && scrambled.isString(1)) {
|
||||
val descrambled = descramble(scrambled.getString(1))
|
||||
JsonParser.array().from(descrambled)
|
||||
} else {
|
||||
scrambled.getArray(0)
|
||||
}
|
||||
|
||||
val messageId = challengeData.getString(0)
|
||||
val interpreterHash = challengeData.getString(3)
|
||||
val program = challengeData.getString(4)
|
||||
val globalName = challengeData.getString(5)
|
||||
val clientExperimentsStateBlob = challengeData.getString(7)
|
||||
|
||||
val privateDoNotAccessOrElseSafeScriptWrappedValue = challengeData.getArray(1, null)?.find { it is String }
|
||||
val privateDoNotAccessOrElseTrustedResourceUrlWrappedValue = challengeData.getArray(2, null)?.find { it is String }
|
||||
|
||||
return JsonWriter.string(
|
||||
JsonObject.builder()
|
||||
.value("messageId", messageId)
|
||||
.`object`("interpreterJavascript")
|
||||
.value("privateDoNotAccessOrElseSafeScriptWrappedValue", privateDoNotAccessOrElseSafeScriptWrappedValue)
|
||||
.value("privateDoNotAccessOrElseTrustedResourceUrlWrappedValue", privateDoNotAccessOrElseTrustedResourceUrlWrappedValue)
|
||||
.end()
|
||||
.value("interpreterHash", interpreterHash)
|
||||
.value("program", program)
|
||||
.value("globalName", globalName)
|
||||
.value("clientExperimentsStateBlob", clientExperimentsStateBlob)
|
||||
.done()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the raw integrity token data obtained from the GenerateIT endpoint to a JavaScript
|
||||
* `Uint8Array` that can be embedded directly in JavaScript code, and an [Int] representing the
|
||||
* duration of this token in seconds.
|
||||
*/
|
||||
fun parseIntegrityTokenData(rawIntegrityTokenData: String): Pair<String, Long> {
|
||||
val integrityTokenData = JsonParser.array().from(rawIntegrityTokenData)
|
||||
return base64ToU8(integrityTokenData.getString(0)) to integrityTokenData.getLong(1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a string (usually the identifier used as input to `obtainPoToken`) to a JavaScript
|
||||
* `Uint8Array` that can be embedded directly in JavaScript code.
|
||||
*/
|
||||
fun stringToU8(identifier: String): String {
|
||||
return newUint8Array(identifier.toByteArray())
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a poToken encoded as a sequence of bytes represented as integers separated by commas
|
||||
* (e.g. "97,98,99" would be "abc"), which is the output of `Uint8Array::toString()` in JavaScript,
|
||||
* and converts it to the specific base64 representation for poTokens.
|
||||
*/
|
||||
fun u8ToBase64(poToken: String): String {
|
||||
return poToken.split(",")
|
||||
.map { it.toUByte().toByte() }
|
||||
.toByteArray()
|
||||
.toByteString()
|
||||
.base64()
|
||||
.replace("+", "-")
|
||||
.replace("/", "_")
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes the scrambled challenge, decodes it from base64, adds 97 to each byte.
|
||||
*/
|
||||
private fun descramble(scrambledChallenge: String): String {
|
||||
return base64ToByteString(scrambledChallenge)
|
||||
.map { (it + 97).toByte() }
|
||||
.toByteArray()
|
||||
.decodeToString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes a base64 string encoded in the specific base64 representation used by YouTube, and
|
||||
* returns a JavaScript `Uint8Array` that can be embedded directly in JavaScript code.
|
||||
*/
|
||||
private fun base64ToU8(base64: String): String {
|
||||
return newUint8Array(base64ToByteString(base64))
|
||||
}
|
||||
|
||||
private fun newUint8Array(contents: ByteArray): String {
|
||||
return "new Uint8Array([" + contents.joinToString(separator = ",") { it.toUByte().toString() } + "])"
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes a base64 string encoded in the specific base64 representation used by YouTube.
|
||||
*/
|
||||
private fun base64ToByteString(base64: String): ByteArray {
|
||||
val base64Mod = base64
|
||||
.replace('-', '+')
|
||||
.replace('_', '/')
|
||||
.replace('.', '=')
|
||||
|
||||
return (base64Mod.decodeBase64() ?: throw PoTokenException("Cannot base64 decode"))
|
||||
.toByteArray()
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package org.schabi.newpipe.util.potoken
|
||||
|
||||
class PoTokenException(message: String) : Exception(message)
|
||||
|
||||
// to be thrown if the WebView provided by the system is broken
|
||||
class BadWebViewException(message: String) : Exception(message)
|
||||
|
||||
fun buildExceptionForJsError(error: String): Exception {
|
||||
return if (error.contains("SyntaxError"))
|
||||
BadWebViewException(error)
|
||||
else
|
||||
PoTokenException(error)
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
package org.schabi.newpipe.util.potoken
|
||||
|
||||
import android.content.Context
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import java.io.Closeable
|
||||
|
||||
/**
|
||||
* This interface was created to allow for multiple methods to generate poTokens in the future (e.g.
|
||||
* via WebView and via a local DOM implementation)
|
||||
*/
|
||||
interface PoTokenGenerator : Closeable {
|
||||
/**
|
||||
* Generates a poToken for the provided identifier, using the `integrityToken` and
|
||||
* `webPoSignalOutput` previously obtained in the initialization of [PoTokenWebView]. Can be
|
||||
* called multiple times.
|
||||
*/
|
||||
fun generatePoToken(identifier: String): Single<String>
|
||||
|
||||
/**
|
||||
* @return whether the `integrityToken` is expired, in which case all tokens generated by
|
||||
* [generatePoToken] will be invalid
|
||||
*/
|
||||
fun isExpired(): Boolean
|
||||
|
||||
interface Factory {
|
||||
/**
|
||||
* Initializes a [PoTokenGenerator] by loading the BotGuard VM, running it, and obtaining
|
||||
* an `integrityToken`. Can then be used multiple times to generate multiple poTokens with
|
||||
* [generatePoToken].
|
||||
*
|
||||
* @param context used e.g. to load the HTML asset or to instantiate a WebView
|
||||
*/
|
||||
fun newPoTokenGenerator(context: Context): Single<PoTokenGenerator>
|
||||
}
|
||||
}
|
@ -0,0 +1,131 @@
|
||||
package org.schabi.newpipe.util.potoken
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import org.schabi.newpipe.App
|
||||
import org.schabi.newpipe.BuildConfig
|
||||
import org.schabi.newpipe.extractor.NewPipe
|
||||
import org.schabi.newpipe.extractor.services.youtube.InnertubeClientRequestInfo
|
||||
import org.schabi.newpipe.extractor.services.youtube.PoTokenProvider
|
||||
import org.schabi.newpipe.extractor.services.youtube.PoTokenResult
|
||||
import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper
|
||||
import org.schabi.newpipe.util.DeviceUtils
|
||||
|
||||
object PoTokenProviderImpl : PoTokenProvider {
|
||||
val TAG = PoTokenProviderImpl::class.simpleName
|
||||
private val webViewSupported by lazy { DeviceUtils.supportsWebView() }
|
||||
private var webViewBadImpl = false // whether the system has a bad WebView implementation
|
||||
|
||||
private object WebPoTokenGenLock
|
||||
private var webPoTokenVisitorData: String? = null
|
||||
private var webPoTokenStreamingPot: String? = null
|
||||
private var webPoTokenGenerator: PoTokenGenerator? = null
|
||||
|
||||
override fun getWebClientPoToken(videoId: String): PoTokenResult? {
|
||||
if (!webViewSupported || webViewBadImpl) {
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
return getWebClientPoToken(videoId = videoId, forceRecreate = false)
|
||||
} catch (e: RuntimeException) {
|
||||
// RxJava's Single wraps exceptions into RuntimeErrors, so we need to unwrap them here
|
||||
when (val cause = e.cause) {
|
||||
is BadWebViewException -> {
|
||||
Log.e(TAG, "Could not obtain poToken because WebView is broken", e)
|
||||
webViewBadImpl = true
|
||||
return null
|
||||
}
|
||||
null -> throw e
|
||||
else -> throw cause // includes PoTokenException
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param forceRecreate whether to force the recreation of [webPoTokenGenerator], to be used in
|
||||
* case the current [webPoTokenGenerator] threw an error last time
|
||||
* [PoTokenGenerator.generatePoToken] was called
|
||||
*/
|
||||
private fun getWebClientPoToken(videoId: String, forceRecreate: Boolean): PoTokenResult {
|
||||
// just a helper class since Kotlin does not have builtin support for 4-tuples
|
||||
data class Quadruple<T1, T2, T3, T4>(val t1: T1, val t2: T2, val t3: T3, val t4: T4)
|
||||
|
||||
val (poTokenGenerator, visitorData, streamingPot, hasBeenRecreated) =
|
||||
synchronized(WebPoTokenGenLock) {
|
||||
val shouldRecreate = webPoTokenGenerator == null || forceRecreate ||
|
||||
webPoTokenGenerator!!.isExpired()
|
||||
|
||||
if (shouldRecreate) {
|
||||
|
||||
val innertubeClientRequestInfo = InnertubeClientRequestInfo.ofWebClient()
|
||||
innertubeClientRequestInfo.clientInfo.clientVersion =
|
||||
YoutubeParsingHelper.getClientVersion()
|
||||
|
||||
webPoTokenVisitorData = YoutubeParsingHelper.getVisitorDataFromInnertube(
|
||||
innertubeClientRequestInfo,
|
||||
NewPipe.getPreferredLocalization(),
|
||||
NewPipe.getPreferredContentCountry(),
|
||||
YoutubeParsingHelper.getYouTubeHeaders(),
|
||||
YoutubeParsingHelper.YOUTUBEI_V1_URL,
|
||||
null,
|
||||
false
|
||||
)
|
||||
// close the current webPoTokenGenerator on the main thread
|
||||
webPoTokenGenerator?.let { Handler(Looper.getMainLooper()).post { it.close() } }
|
||||
|
||||
// create a new webPoTokenGenerator
|
||||
webPoTokenGenerator = PoTokenWebView
|
||||
.newPoTokenGenerator(App.getApp()).blockingGet()
|
||||
|
||||
// The streaming poToken needs to be generated exactly once before generating
|
||||
// any other (player) tokens.
|
||||
webPoTokenStreamingPot = webPoTokenGenerator!!
|
||||
.generatePoToken(webPoTokenVisitorData!!).blockingGet()
|
||||
}
|
||||
|
||||
return@synchronized Quadruple(
|
||||
webPoTokenGenerator!!,
|
||||
webPoTokenVisitorData!!,
|
||||
webPoTokenStreamingPot!!,
|
||||
shouldRecreate
|
||||
)
|
||||
}
|
||||
|
||||
val playerPot = try {
|
||||
// Not using synchronized here, since poTokenGenerator would be able to generate
|
||||
// multiple poTokens in parallel if needed. The only important thing is for exactly one
|
||||
// visitorData/streaming poToken to be generated before anything else.
|
||||
poTokenGenerator.generatePoToken(videoId).blockingGet()
|
||||
} catch (throwable: Throwable) {
|
||||
if (hasBeenRecreated) {
|
||||
// the poTokenGenerator has just been recreated (and possibly this is already the
|
||||
// second time we try), so there is likely nothing we can do
|
||||
throw throwable
|
||||
} else {
|
||||
// retry, this time recreating the [webPoTokenGenerator] from scratch;
|
||||
// this might happen for example if NewPipe goes in the background and the WebView
|
||||
// content is lost
|
||||
Log.e(TAG, "Failed to obtain poToken, retrying", throwable)
|
||||
return getWebClientPoToken(videoId = videoId, forceRecreate = true)
|
||||
}
|
||||
}
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(
|
||||
TAG,
|
||||
"poToken for $videoId: playerPot=$playerPot, " +
|
||||
"streamingPot=$streamingPot, visitor_data=$visitorData"
|
||||
)
|
||||
}
|
||||
|
||||
return PoTokenResult(visitorData, playerPot, streamingPot)
|
||||
}
|
||||
|
||||
override fun getWebEmbedClientPoToken(videoId: String): PoTokenResult? = null
|
||||
|
||||
override fun getAndroidClientPoToken(videoId: String): PoTokenResult? = null
|
||||
|
||||
override fun getIosClientPoToken(videoId: String): PoTokenResult? = null
|
||||
}
|
@ -0,0 +1,395 @@
|
||||
package org.schabi.newpipe.util.potoken
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import android.webkit.ConsoleMessage
|
||||
import android.webkit.JavascriptInterface
|
||||
import android.webkit.WebChromeClient
|
||||
import android.webkit.WebView
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.webkit.WebSettingsCompat
|
||||
import androidx.webkit.WebViewFeature
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.core.SingleEmitter
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.schabi.newpipe.BuildConfig
|
||||
import org.schabi.newpipe.DownloaderImpl
|
||||
import java.time.Instant
|
||||
|
||||
class PoTokenWebView private constructor(
|
||||
context: Context,
|
||||
// to be used exactly once only during initialization!
|
||||
private val generatorEmitter: SingleEmitter<PoTokenGenerator>,
|
||||
) : PoTokenGenerator {
|
||||
private val webView = WebView(context)
|
||||
private val disposables = CompositeDisposable() // used only during initialization
|
||||
private val poTokenEmitters = mutableListOf<Pair<String, SingleEmitter<String>>>()
|
||||
private lateinit var expirationInstant: Instant
|
||||
|
||||
//region Initialization
|
||||
init {
|
||||
val webViewSettings = webView.settings
|
||||
//noinspection SetJavaScriptEnabled we want to use JavaScript!
|
||||
webViewSettings.javaScriptEnabled = true
|
||||
if (WebViewFeature.isFeatureSupported(WebViewFeature.SAFE_BROWSING_ENABLE)) {
|
||||
WebSettingsCompat.setSafeBrowsingEnabled(webViewSettings, false)
|
||||
}
|
||||
webViewSettings.userAgentString = USER_AGENT
|
||||
webViewSettings.blockNetworkLoads = true // the WebView does not need internet access
|
||||
|
||||
// so that we can run async functions and get back the result
|
||||
webView.addJavascriptInterface(this, JS_INTERFACE)
|
||||
|
||||
webView.webChromeClient = object : WebChromeClient() {
|
||||
override fun onConsoleMessage(m: ConsoleMessage): Boolean {
|
||||
if (m.message().contains("Uncaught")) {
|
||||
// There should not be any uncaught errors while executing the code, because
|
||||
// everything that can fail is guarded by try-catch. Therefore, this likely
|
||||
// indicates that there was a syntax error in the code, i.e. the WebView only
|
||||
// supports a really old version of JS.
|
||||
|
||||
val fmt = "\"${m.message()}\", source: ${m.sourceId()} (${m.lineNumber()})"
|
||||
val exception = BadWebViewException(fmt)
|
||||
Log.e(TAG, "This WebView implementation is broken: $fmt")
|
||||
|
||||
onInitializationErrorCloseAndCancel(exception)
|
||||
popAllPoTokenEmitters().forEach { (_, emitter) -> emitter.onError(exception) }
|
||||
}
|
||||
return super.onConsoleMessage(m)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Must be called right after instantiating [PoTokenWebView] to perform the actual
|
||||
* initialization. This will asynchronously go through all the steps needed to load BotGuard,
|
||||
* run it, and obtain an `integrityToken`.
|
||||
*/
|
||||
private fun loadHtmlAndObtainBotguard(context: Context) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(TAG, "loadHtmlAndObtainBotguard() called")
|
||||
}
|
||||
|
||||
disposables.add(
|
||||
Single.fromCallable {
|
||||
val html = context.assets.open("po_token.html").bufferedReader()
|
||||
.use { it.readText() }
|
||||
return@fromCallable html
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ html ->
|
||||
webView.loadDataWithBaseURL(
|
||||
"https://www.youtube.com",
|
||||
html.replaceFirst(
|
||||
"</script>",
|
||||
// calls downloadAndRunBotguard() when the page has finished loading
|
||||
"\n$JS_INTERFACE.downloadAndRunBotguard()</script>"
|
||||
),
|
||||
"text/html",
|
||||
"utf-8",
|
||||
null,
|
||||
)
|
||||
},
|
||||
this::onInitializationErrorCloseAndCancel
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called during initialization by the JavaScript snippet appended to the HTML page content in
|
||||
* [loadHtmlAndObtainBotguard] after the WebView content has been loaded.
|
||||
*/
|
||||
@JavascriptInterface
|
||||
fun downloadAndRunBotguard() {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(TAG, "downloadAndRunBotguard() called")
|
||||
}
|
||||
|
||||
makeBotguardServiceRequest(
|
||||
"https://www.youtube.com/api/jnn/v1/Create",
|
||||
"[ \"$REQUEST_KEY\" ]",
|
||||
) { responseBody ->
|
||||
val parsedChallengeData = parseChallengeData(responseBody)
|
||||
webView.evaluateJavascript(
|
||||
"""try {
|
||||
data = $parsedChallengeData
|
||||
runBotGuard(data).then(function (result) {
|
||||
this.webPoSignalOutput = result.webPoSignalOutput
|
||||
$JS_INTERFACE.onRunBotguardResult(result.botguardResponse)
|
||||
}, function (error) {
|
||||
$JS_INTERFACE.onJsInitializationError(error + "\n" + error.stack)
|
||||
})
|
||||
} catch (error) {
|
||||
$JS_INTERFACE.onJsInitializationError(error + "\n" + error.stack)
|
||||
}""",
|
||||
null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called during initialization by the JavaScript snippets from either
|
||||
* [downloadAndRunBotguard] or [onRunBotguardResult].
|
||||
*/
|
||||
@JavascriptInterface
|
||||
fun onJsInitializationError(error: String) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.e(TAG, "Initialization error from JavaScript: $error")
|
||||
}
|
||||
onInitializationErrorCloseAndCancel(buildExceptionForJsError(error))
|
||||
}
|
||||
|
||||
/**
|
||||
* Called during initialization by the JavaScript snippet from [downloadAndRunBotguard] after
|
||||
* obtaining the BotGuard execution output [botguardResponse].
|
||||
*/
|
||||
@JavascriptInterface
|
||||
fun onRunBotguardResult(botguardResponse: String) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(TAG, "botguardResponse: $botguardResponse")
|
||||
}
|
||||
makeBotguardServiceRequest(
|
||||
"https://www.youtube.com/api/jnn/v1/GenerateIT",
|
||||
"[ \"$REQUEST_KEY\", \"$botguardResponse\" ]",
|
||||
) { responseBody ->
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(TAG, "GenerateIT response: $responseBody")
|
||||
}
|
||||
val (integrityToken, expirationTimeInSeconds) = parseIntegrityTokenData(responseBody)
|
||||
|
||||
// leave 10 minutes of margin just to be sure
|
||||
expirationInstant = Instant.now().plusSeconds(expirationTimeInSeconds - 600)
|
||||
|
||||
webView.evaluateJavascript(
|
||||
"this.integrityToken = $integrityToken"
|
||||
) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(TAG, "initialization finished, expiration=${expirationTimeInSeconds}s")
|
||||
}
|
||||
generatorEmitter.onSuccess(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region Obtaining poTokens
|
||||
override fun generatePoToken(identifier: String): Single<String> =
|
||||
Single.create { emitter ->
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(TAG, "generatePoToken() called with identifier $identifier")
|
||||
}
|
||||
runOnMainThread(emitter) {
|
||||
addPoTokenEmitter(identifier, emitter)
|
||||
val u8Identifier = stringToU8(identifier)
|
||||
webView.evaluateJavascript(
|
||||
"""try {
|
||||
identifier = "$identifier"
|
||||
u8Identifier = $u8Identifier
|
||||
poTokenU8 = obtainPoToken(webPoSignalOutput, integrityToken, u8Identifier)
|
||||
poTokenU8String = ""
|
||||
for (i = 0; i < poTokenU8.length; i++) {
|
||||
if (i != 0) poTokenU8String += ","
|
||||
poTokenU8String += poTokenU8[i]
|
||||
}
|
||||
$JS_INTERFACE.onObtainPoTokenResult(identifier, poTokenU8String)
|
||||
} catch (error) {
|
||||
$JS_INTERFACE.onObtainPoTokenError(identifier, error + "\n" + error.stack)
|
||||
}""",
|
||||
) {}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by the JavaScript snippet from [generatePoToken] when an error occurs in calling the
|
||||
* JavaScript `obtainPoToken()` function.
|
||||
*/
|
||||
@JavascriptInterface
|
||||
fun onObtainPoTokenError(identifier: String, error: String) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.e(TAG, "obtainPoToken error from JavaScript: $error")
|
||||
}
|
||||
popPoTokenEmitter(identifier)?.onError(buildExceptionForJsError(error))
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by the JavaScript snippet from [generatePoToken] with the original identifier and the
|
||||
* result of the JavaScript `obtainPoToken()` function.
|
||||
*/
|
||||
@JavascriptInterface
|
||||
fun onObtainPoTokenResult(identifier: String, poTokenU8: String) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(TAG, "Generated poToken (before decoding): identifier=$identifier poTokenU8=$poTokenU8")
|
||||
}
|
||||
val poToken = try {
|
||||
u8ToBase64(poTokenU8)
|
||||
} catch (t: Throwable) {
|
||||
popPoTokenEmitter(identifier)?.onError(t)
|
||||
return
|
||||
}
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(TAG, "Generated poToken: identifier=$identifier poToken=$poToken")
|
||||
}
|
||||
popPoTokenEmitter(identifier)?.onSuccess(poToken)
|
||||
}
|
||||
|
||||
override fun isExpired(): Boolean {
|
||||
return Instant.now().isAfter(expirationInstant)
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region Handling multiple emitters
|
||||
/**
|
||||
* Adds the ([identifier], [emitter]) pair to the [poTokenEmitters] list. This makes it so that
|
||||
* multiple poToken requests can be generated invparallel, and the results will be notified to
|
||||
* the right emitters.
|
||||
*/
|
||||
private fun addPoTokenEmitter(identifier: String, emitter: SingleEmitter<String>) {
|
||||
synchronized(poTokenEmitters) {
|
||||
poTokenEmitters.add(Pair(identifier, emitter))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts and removes from the [poTokenEmitters] list a [SingleEmitter] based on its
|
||||
* [identifier]. The emitter is supposed to be used immediately after to either signal a success
|
||||
* or an error.
|
||||
*/
|
||||
private fun popPoTokenEmitter(identifier: String): SingleEmitter<String>? {
|
||||
return synchronized(poTokenEmitters) {
|
||||
poTokenEmitters.indexOfFirst { it.first == identifier }.takeIf { it >= 0 }?.let {
|
||||
poTokenEmitters.removeAt(it).second
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears [poTokenEmitters] and returns its previous contents. The emitters are supposed to be
|
||||
* used immediately after to either signal a success or an error.
|
||||
*/
|
||||
private fun popAllPoTokenEmitters(): List<Pair<String, SingleEmitter<String>>> {
|
||||
return synchronized(poTokenEmitters) {
|
||||
val result = poTokenEmitters.toList()
|
||||
poTokenEmitters.clear()
|
||||
result
|
||||
}
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region Utils
|
||||
/**
|
||||
* Makes a POST request to [url] with the given [data] by setting the correct headers. Calls
|
||||
* [onInitializationErrorCloseAndCancel] in case of any network errors and also if the response
|
||||
* does not have HTTP code 200, therefore this is supposed to be used only during
|
||||
* initialization. Calls [handleResponseBody] with the response body if the response is
|
||||
* successful. The request is performed in the background and a disposable is added to
|
||||
* [disposables].
|
||||
*/
|
||||
private fun makeBotguardServiceRequest(
|
||||
url: String,
|
||||
data: String,
|
||||
handleResponseBody: (String) -> Unit,
|
||||
) {
|
||||
disposables.add(
|
||||
Single.fromCallable {
|
||||
return@fromCallable DownloaderImpl.getInstance().post(
|
||||
url,
|
||||
mapOf(
|
||||
// replace the downloader user agent
|
||||
"User-Agent" to listOf(USER_AGENT),
|
||||
"Accept" to listOf("application/json"),
|
||||
"Content-Type" to listOf("application/json+protobuf"),
|
||||
"x-goog-api-key" to listOf(GOOGLE_API_KEY),
|
||||
"x-user-agent" to listOf("grpc-web-javascript/0.1"),
|
||||
),
|
||||
data.toByteArray()
|
||||
)
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ response ->
|
||||
val httpCode = response.responseCode()
|
||||
if (httpCode != 200) {
|
||||
onInitializationErrorCloseAndCancel(
|
||||
PoTokenException("Invalid response code: $httpCode")
|
||||
)
|
||||
return@subscribe
|
||||
}
|
||||
val responseBody = response.responseBody()
|
||||
handleResponseBody(responseBody)
|
||||
},
|
||||
this::onInitializationErrorCloseAndCancel
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles any error happening during initialization, releasing resources and sending the error
|
||||
* to [generatorEmitter].
|
||||
*/
|
||||
private fun onInitializationErrorCloseAndCancel(error: Throwable) {
|
||||
runOnMainThread(generatorEmitter) {
|
||||
close()
|
||||
generatorEmitter.onError(error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Releases all [webView] and [disposables] resources.
|
||||
*/
|
||||
@MainThread
|
||||
override fun close() {
|
||||
disposables.dispose()
|
||||
|
||||
webView.clearHistory()
|
||||
// clears RAM cache and disk cache (globally for all WebViews)
|
||||
webView.clearCache(true)
|
||||
|
||||
// ensures that the WebView isn't doing anything when destroying it
|
||||
webView.loadUrl("about:blank")
|
||||
|
||||
webView.onPause()
|
||||
webView.removeAllViews()
|
||||
webView.destroy()
|
||||
}
|
||||
//endregion
|
||||
|
||||
companion object : PoTokenGenerator.Factory {
|
||||
private val TAG = PoTokenWebView::class.simpleName
|
||||
// Public API key used by BotGuard, which has been got by looking at BotGuard requests
|
||||
private const val GOOGLE_API_KEY = "AIzaSyDyT5W0Jh49F30Pqqtyfdf7pDLFKLJoAnw" // NOSONAR
|
||||
private const val REQUEST_KEY = "O43z0dpjhgX20SCx4KAo"
|
||||
private const val USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " +
|
||||
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.3"
|
||||
private const val JS_INTERFACE = "PoTokenWebView"
|
||||
|
||||
override fun newPoTokenGenerator(context: Context): Single<PoTokenGenerator> =
|
||||
Single.create { emitter ->
|
||||
runOnMainThread(emitter) {
|
||||
val potWv = PoTokenWebView(context, emitter)
|
||||
potWv.loadHtmlAndObtainBotguard(context)
|
||||
emitter.setDisposable(potWv.disposables)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs [runnable] on the main thread using `Handler(Looper.getMainLooper()).post()`, and
|
||||
* if the `post` fails emits an error on [emitterIfPostFails].
|
||||
*/
|
||||
private fun runOnMainThread(
|
||||
emitterIfPostFails: SingleEmitter<out Any>,
|
||||
runnable: Runnable,
|
||||
) {
|
||||
if (!Handler(Looper.getMainLooper()).post(runnable)) {
|
||||
emitterIfPostFails.onError(PoTokenException("Could not run on main thread"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -71,6 +71,9 @@ import java.net.URI;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Iterator;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
import java.text.DateFormat;
|
||||
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Observable;
|
||||
@ -208,11 +211,17 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
|
||||
h.pause.setTitle(mission.unknownLength ? R.string.stop : R.string.pause);
|
||||
updateProgress(h);
|
||||
mPendingDownloadsItems.add(h);
|
||||
|
||||
h.date.setText("");
|
||||
} else {
|
||||
h.progress.setMarquee(false);
|
||||
h.status.setText("100%");
|
||||
h.progress.setProgress(1.0f);
|
||||
h.size.setText(Utility.formatBytes(item.mission.length));
|
||||
|
||||
DateFormat dateFormat = DateFormat.getDateInstance(DateFormat.MEDIUM, Locale.getDefault());
|
||||
Date date = new Date(item.mission.timestamp);
|
||||
h.date.setText(dateFormat.format(date));
|
||||
}
|
||||
}
|
||||
|
||||
@ -664,6 +673,13 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
|
||||
return true;
|
||||
case R.id.md5:
|
||||
case R.id.sha1:
|
||||
final StoredFileHelper storage = h.item.mission.storage;
|
||||
if (!storage.existsAsFile()) {
|
||||
Toast.makeText(mContext, R.string.missing_file, Toast.LENGTH_SHORT).show();
|
||||
mDeleter.append(h.item.mission);
|
||||
applyChanges();
|
||||
return true;
|
||||
}
|
||||
final NotificationManager notificationManager
|
||||
= ContextCompat.getSystemService(mContext, NotificationManager.class);
|
||||
final NotificationCompat.Builder progressNotificationBuilder
|
||||
@ -678,7 +694,6 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
|
||||
|
||||
notificationManager.notify(HASH_NOTIFICATION_ID, progressNotificationBuilder
|
||||
.build());
|
||||
final StoredFileHelper storage = h.item.mission.storage;
|
||||
compositeDisposable.add(
|
||||
Observable.fromCallable(() -> Utility.checksum(storage, id))
|
||||
.subscribeOn(Schedulers.computation())
|
||||
@ -826,6 +841,7 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
|
||||
ImageView icon;
|
||||
TextView name;
|
||||
TextView size;
|
||||
TextView date;
|
||||
ProgressDrawable progress;
|
||||
|
||||
PopupMenu popupMenu;
|
||||
@ -856,6 +872,7 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
|
||||
name = itemView.findViewById(R.id.item_name);
|
||||
icon = itemView.findViewById(R.id.item_icon);
|
||||
size = itemView.findViewById(R.id.item_size);
|
||||
date = itemView.findViewById(R.id.item_date);
|
||||
|
||||
name.setSelected(true);
|
||||
|
||||
|
9
app/src/main/res/drawable-mdpi/volunteer_activism_ic.xml
Normal file
9
app/src/main/res/drawable-mdpi/volunteer_activism_ic.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:pathData="M640,520 L474,358q-31,-30 -52.5,-66.5T400,212q0,-55 38.5,-93.5T532,80q32,0 60,13.5t48,36.5q20,-23 48,-36.5t60,-13.5q55,0 93.5,38.5T880,212q0,43 -21,79.5T807,358L640,520ZM640,408 L749,301q19,-19 35,-40.5t16,-48.5q0,-22 -15,-37t-37,-15q-14,0 -26.5,5.5T700,182l-60,72 -60,-72q-9,-11 -21.5,-16.5T532,160q-22,0 -37,15t-15,37q0,27 16,48.5t35,40.5l109,107ZM280,740l278,76 238,-74q-5,-9 -14.5,-15.5T760,720L558,720q-27,0 -43,-2t-33,-8l-93,-31 22,-78 81,27q17,5 40,8t68,4q0,-11 -6.5,-21T578,606l-234,-86h-64v220ZM40,880v-440h304q7,0 14,1.5t13,3.5l235,87q33,12 53.5,42t20.5,66h80q50,0 85,33t35,87v40L560,900l-280,-78v58L40,880ZM120,800h80v-280h-80v280ZM640,254Z"
|
||||
android:fillColor="#FF000000"/>
|
||||
</vector>
|
10
app/src/main/res/drawable/ic_bookmark_white.xml
Normal file
10
app/src/main/res/drawable/ic_bookmark_white.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="@color/white"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M17,3H7c-1.1,0 -1.99,0.9 -1.99,2L5,21l7,-3 7,3V5c0,-1.1 -0.9,-2 -2,-2z" />
|
||||
</vector>
|
10
app/src/main/res/drawable/ic_history_white.xml
Normal file
10
app/src/main/res/drawable/ic_history_white.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="@color/white"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M13,3c-4.97,0 -9,4.03 -9,9L1,12l3.89,3.89 0.07,0.14L9,12L6,12c0,-3.87 3.13,-7 7,-7s7,3.13 7,7 -3.13,7 -7,7c-1.93,0 -3.68,-0.79 -4.94,-2.06l-1.42,1.42C8.27,19.99 10.51,21 13,21c4.97,0 9,-4.03 9,-9s-4.03,-9 -9,-9zM12,8v5l4.28,2.54 0.72,-1.21 -3.5,-2.08L13.5,8L12,8z" />
|
||||
</vector>
|
@ -82,6 +82,18 @@
|
||||
android:textColor="@color/white"
|
||||
android:textSize="12sp" />
|
||||
|
||||
<org.schabi.newpipe.views.NewPipeTextView
|
||||
android:id="@+id/item_date"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/item_name"
|
||||
android:layout_alignParentRight="true"
|
||||
android:padding="6dp"
|
||||
android:singleLine="true"
|
||||
android:text=""
|
||||
android:textColor="@color/white"
|
||||
android:textSize="12sp" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
</RelativeLayout>
|
||||
|
@ -62,6 +62,18 @@
|
||||
android:textSize="12sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<org.schabi.newpipe.views.NewPipeTextView
|
||||
android:id="@+id/item_date"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/item_name"
|
||||
android:layout_toLeftOf="@id/item_more"
|
||||
android:padding="6dp"
|
||||
android:singleLine="true"
|
||||
android:text=""
|
||||
android:textColor="@color/white"
|
||||
android:textSize="12sp" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/item_more"
|
||||
style="?attr/buttonBarButtonStyle"
|
||||
|
1
app/src/main/res/resources.properties
Normal file
1
app/src/main/res/resources.properties
Normal file
@ -0,0 +1 @@
|
||||
unqualifiedResLocale=en-US
|
@ -856,6 +856,5 @@
|
||||
<string name="share_playlist_content_details">%1$s
|
||||
\n%2$s</string>
|
||||
<string name="share_playlist">شارِك قائمة التشغيل</string>
|
||||
<string name="share_playlist_with_titles_message">شارِك قائمة التشغيل بتفاصيليها مثل اسم قائمة التشغيل وعناوين الفيديو أو كقائمة بسيطة من عناوين تشعّبيّة للفيديوهات</string>
|
||||
<string name="video_details_list_item">- %1$s: %2$s</string>
|
||||
</resources>
|
@ -18,7 +18,7 @@
|
||||
<string name="download_path_dialog_title">اختر مجلد التنزيل لملفات الفيديو</string>
|
||||
<string name="download_path_summary">يتم تخزين ملفات الفيديو التي تم تنزيلها هنا</string>
|
||||
<string name="download_path_title">مجلد تحميل الفيديو</string>
|
||||
<string name="install">ثبت</string>
|
||||
<string name="install">ثبيت</string>
|
||||
<string name="kore_not_found">تطبيق Kore غير موجود. هل تريد تثبيته؟</string>
|
||||
<string name="light_theme_title">فاتح</string>
|
||||
<string name="network_error">خطأ في الشبكة</string>
|
||||
@ -83,7 +83,7 @@
|
||||
<string name="resume_on_audio_focus_gain_title">استئناف التشغيل</string>
|
||||
<string name="resume_on_audio_focus_gain_summary">متابعة التشغيل بعد المقاطعات (مثل المكالمات الهاتفية)</string>
|
||||
<string name="show_hold_to_append_title">إظهار تلميح \"اضغط للفتح\"</string>
|
||||
<string name="show_hold_to_append_summary">عرض تلميح عند الضغط على زر استخدام المشغل الخلفي أو النافذة المنبثقة في صفحة تفاصيل الفديو</string>
|
||||
<string name="show_hold_to_append_summary">إظهار التلميح عند الضغط على الخلفية أو الزر المنبثق في الفيديو \"التفاصيل:\"</string>
|
||||
<string name="settings_category_player_title">المشغل</string>
|
||||
<string name="settings_category_player_behavior_title">السلوك</string>
|
||||
<string name="popup_playing_toast">تشغيل في وضع منبثق</string>
|
||||
@ -856,7 +856,6 @@
|
||||
<string name="share_playlist_content_details">%1$s
|
||||
\n%2$s</string>
|
||||
<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>
|
||||
@ -881,4 +880,5 @@
|
||||
\nهل تريد تمكين هذا؟</string>
|
||||
<string name="no">لا</string>
|
||||
<string name="import_settings_vulnerable_format">تستخدم الإعدادات الموجودة في عملية التصدير التي يتم استيرادها تنسيقًا عرضة للاختراق تم إهماله منذ NewPipe 0.27.0. تأكد من أن التصدير الذي يتم استيراده من مصدر موثوق به، ويفضل استخدام عمليات التصدير التي تم الحصول عليها من NewPipe 0.27.0 أو الأحدث في المستقبل فقط. سيتم قريبًا إزالة دعم استيراد الإعدادات بهذا التنسيق الضعيف تمامًا، وبعد ذلك لن تتمكن الإصدارات القديمة من NewPipe من استيراد إعدادات التصدير من الإصدارات الجديدة بعد الآن.</string>
|
||||
<string name="audio_track_type_secondary">الثانوي</string>
|
||||
</resources>
|
@ -604,7 +604,7 @@
|
||||
<string name="export_to">Bura ixrac et</string>
|
||||
<string name="import_file_title">Faylı idxal et</string>
|
||||
<string name="subscriptions_import_unsuccessful">Abunəlikləri idxal etmək mümkün olmadı</string>
|
||||
<string name="start_accept_privacy_policy">Avropa Ümumi Məlumat Mühafizəsi Qaydasına (GDPR) riayət etmək üçün diqqətinizi NewPipe məxfilik siyasətinə cəlb edirik. Xahiş edirik, diqqətlə oxuyun. \nXəta məlumatın bizə göndərmək üçün qəbul etməlisiniz.</string>
|
||||
<string name="start_accept_privacy_policy">Avropa Ümumi Məlumat Mühafizəsi Qaydasına (GDPR) riayət etmək üçün diqqətinizi NewPipe məxfilik siyasətinə cəlb edirik. Xahiş edirik, diqqətlə oxuyun.\nXəta məlumatın bizə göndərmək üçün qəbul etməlisiniz.</string>
|
||||
<string name="overwrite_unrelated_warning">Bu adda fayl artıq mövcuddur</string>
|
||||
<string name="download_already_pending">Bu adla gözlənilən bir endirmə var</string>
|
||||
<string name="error_path_creation">Təyinat qovluğu yaradıla bilməz</string>
|
||||
@ -669,7 +669,7 @@
|
||||
<string name="feed_use_dedicated_fetch_method_summary">Bəzi xidmətlərdə mövcuddur, adətən daha sürətli olur, lakin məhdud sayda elementləri və çox vaxt natamam məlumatı qaytara bilər (məsələn, müddət, element növü, canlı status yoxdur)</string>
|
||||
<string name="no_appropriate_file_manager_message">Bu əməliyyat üçün uyğun fayl meneceri tapılmadı. Zəhmət olmasa, fayl menecerini quraşdır və ya endirmə tənzimləmələrində \'%s\'-i qeyri-aktiv etməyə çalış</string>
|
||||
<string name="feed_load_error_account_info">\'%s\' üçün axın yükləmək mümkün olmadı.</string>
|
||||
<string name="no_appropriate_file_manager_message_android_10">Bu fəaliyyət üçün uyğun fayl meneceri tapılmadı.\nXahiş olunur, Yaddaş Giriş Quruluşuna uyğun fayl meneceri quraşdırın.</string>
|
||||
<string name="no_appropriate_file_manager_message_android_10">Bu fəaliyyət üçün uyğun fayl meneceri tapılmadı.\nXahiş olunur, Yaddaş Giriş Quruluşuna uyğun fayl meneceri quraşdırın</string>
|
||||
<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>
|
||||
@ -743,7 +743,7 @@
|
||||
<string name="feed_fetch_channel_tabs">Kanal səhifələrin əldə et</string>
|
||||
<string name="metadata_avatars">Avatarlar</string>
|
||||
<string name="metadata_subchannel_avatars">Alt kanal avatarları</string>
|
||||
<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="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ş fəaliyyətin ona toxunub redaktə edin. İlk üç fəaliyyət (oynatma/fasilə, əvvəlki və növbəti) sistem tərəfindən tənzimlənib və dəyişdirilə bilməz.</string>
|
||||
@ -787,7 +787,6 @@
|
||||
<string name="image_quality_high">Yüksək keyfiyyət</string>
|
||||
<string name="question_mark">\?</string>
|
||||
<string name="share_playlist">Oynatma siyahısın paylaş</string>
|
||||
<string name="share_playlist_with_titles_message">Pleylist adı və video başlıqları kimi təfsilatlar və ya video URL-lərin sadə siyahısı olaraq pleylist paylaş</string>
|
||||
<string name="share_playlist_with_titles">Başlıqlarla paylaşın</string>
|
||||
<string name="video_details_list_item">- %1$s: %2$s</string>
|
||||
<string name="share_playlist_content_details">%1$s
|
||||
|
@ -26,4 +26,6 @@
|
||||
<string name="use_external_video_player_summary">Duad bei manchen Auflösungen d\'Tonspur weggad</string>
|
||||
<string name="open_in_popup_mode">Im Pop-up Modus aufmacha</string>
|
||||
<string name="main_bg_subtitle">Drug auf\'d Lubn zum ofanga.</string>
|
||||
<string name="ok">Bassd scho</string>
|
||||
<string name="no">naa</string>
|
||||
</resources>
|
File diff suppressed because it is too large
Load Diff
@ -60,7 +60,7 @@
|
||||
<string name="show_next_and_similar_title">Покажи „следващ“ и „подобни“</string>
|
||||
<string name="show_hold_to_append_title">Покажи съвет „Задръжте за поставяне в опашка“</string>
|
||||
<string name="unsupported_url">Непознат URL</string>
|
||||
<string name="content_language_title">Език на съдържанието по подразбиране</string>
|
||||
<string name="content_language_title">Език на съдържание по подразбиране</string>
|
||||
<string name="settings_category_player_title">Плейър</string>
|
||||
<string name="settings_category_player_behavior_title">Поведение</string>
|
||||
<string name="settings_category_video_audio_title">Видео и аудио</string>
|
||||
@ -192,7 +192,7 @@
|
||||
<string name="copyright" formatted="true">© %1$s от %2$s под лиценза %3$s</string>
|
||||
<string name="contribution_title">Съдействайте</string>
|
||||
<string name="contribution_encouragement">За всичко, което се сетите: превод, промени по дизайна, изчистване на кода или много сериозни промени по кода – помощта е винаги добре дошла. Колкото повече развитие, толкова по-добре!</string>
|
||||
<string name="donation_title">Направете дарение</string>
|
||||
<string name="donation_title">Дарение</string>
|
||||
<string name="donation_encouragement">NewPipe се разработва от доброволци, които отделят от своето време, за да предоставят най-доброто потребителско изживяване. Включете се в разработката като почерпите разработчиците с една чашка кафе, които да изпият, докато правят NewPipe още по-добро приложение.</string>
|
||||
<string name="give_back">Дари</string>
|
||||
<string name="website_title">Уебсайт</string>
|
||||
@ -203,7 +203,7 @@
|
||||
<string name="read_privacy_policy">Прочетете нашата политика за поверителност</string>
|
||||
<string name="app_license_title">Лицензът на NewPipe</string>
|
||||
<string name="no_player_found_toast">Липсва стрийм плейър (можете да изтеглите VLC, за да пуснете стрийма).</string>
|
||||
<string name="show_hold_to_append_summary">Покажи съвет при натискане на фона или изскачащия бутон във видеоклипа „Подробности:“</string>
|
||||
<string name="show_hold_to_append_summary">Покажи съвет при натискане на фона или изскачащия бутон във видеоклипа \"Подробности:“</string>
|
||||
<string name="clear_views_history_summary">Изтрива историята на възпроизвежданите стриймове и позицията на възпроизвеждането</string>
|
||||
<string name="video_streams_empty">Не са намерени видео стриймове</string>
|
||||
<string name="audio_streams_empty">Не са намерени аудио стриймове</string>
|
||||
@ -519,7 +519,7 @@
|
||||
<string name="pause_downloads_on_mobile_desc">Полезно при превключване към мобилни данни, въпреки че някои изтегляния не поддържат възобновяване и ще започнат отначало</string>
|
||||
<string name="crash_the_app">Срив на приложението</string>
|
||||
<string name="notification_colorize_summary">Цветът на известието да се избира според главния цвят в миниатюрата на видеото (може да не работи на всички устройства)</string>
|
||||
<string name="youtube_restricted_mode_enabled_title">Използване на ограничения режим на YouTube</string>
|
||||
<string name="youtube_restricted_mode_enabled_title">Включване на \"Ограничен режим“ в YouTube</string>
|
||||
<string name="youtube_restricted_mode_enabled_summary">YouTube предлага „ограничен режим“, чрез който можете да филтрирате потенциално съдържание за възрастни</string>
|
||||
<string name="restricted_video">Това видео е с възрастова граница.
|
||||
\n
|
||||
@ -558,7 +558,7 @@
|
||||
<string name="enable_playback_state_lists_summary">Покажи индикатори за позиция на възпроизвеждане в списъци</string>
|
||||
<string name="notification_actions_summary_android13">Редактирайте всяко действие за известяване по-долу, като щракнете върху него. Първите три действия (възпроизвеждане/пауза, предишно и следващо) се задават от системата и не могат да бъдат конфигурирани.</string>
|
||||
<string name="right_gesture_control_summary">Изберете жест за дясната половина на екрана на плейъра</string>
|
||||
<string name="right_gesture_control_title">Действие с жест на дясно</string>
|
||||
<string name="right_gesture_control_title">Действие с жест надясно</string>
|
||||
<string name="start_main_player_fullscreen_title">Стартирайте основния плейър на цял екран</string>
|
||||
<string name="streams_notification_channel_description">Известия за нови видеоклипове в абонаментите</string>
|
||||
<string name="enable_streams_notifications_summary">Известявайте за нови видеоклипове в абонаментите</string>
|
||||
@ -583,9 +583,7 @@
|
||||
<string name="remove_duplicates_title">Премахни повторения?</string>
|
||||
<string name="feed_groups_header_title">Група от канали</string>
|
||||
<string name="feed_load_error_account_info">Неуспешно зареждане на емисия за \'%s\'.</string>
|
||||
<string name="feed_load_error_terminated">Акаунтът на автора е бил отстранен.
|
||||
\nNewPipe няма да може да зареди тази емисия вече.
|
||||
\nИскате ли да махнете абонамента от този канал?</string>
|
||||
<string name="feed_load_error_terminated">Профилът на автора е бил отстранен. \nNewPipe няма да може да зареди тази емисия вече. \nИскате ли да махнете абонамента от този канал?</string>
|
||||
<string name="feed_update_threshold_option_always_update">Винаги опреснявай</string>
|
||||
<string name="feed_load_error">Грешка при зареждане на емисия</string>
|
||||
<string name="feed_show_hide_streams">Покажи/Скрий потоци</string>
|
||||
@ -694,7 +692,7 @@
|
||||
<string name="card">Карта</string>
|
||||
<string name="delete_downloaded_files_confirm">Изтрий всички изтеглени файлове от диска?</string>
|
||||
<string name="enable_queue_limit_desc">Едно изтегляне ще се изпълнява едновременно</string>
|
||||
<string name="downloads_storage_ask_title">Попитайте къде да изтеглите</string>
|
||||
<string name="downloads_storage_ask_title">Подкана за папка за изтегляне</string>
|
||||
<string name="systems_language">Система по подразбиране</string>
|
||||
<string name="remove_duplicates">Премахване на дубликати</string>
|
||||
<string name="remove_duplicates_message">Искате ли да премахнете всички дублиращи се потоци в този плейлист?</string>
|
||||
@ -716,7 +714,6 @@
|
||||
<string name="replay">Повторение</string>
|
||||
<string name="rewind">Превъртане назад</string>
|
||||
<string name="forward">Напред</string>
|
||||
<string name="share_playlist_with_titles_message">Споделете плейлист с подробности, като име на плейлист и заглавия на видеоклипове или като обикновен списък с URL адреси на видеоклипове</string>
|
||||
<string name="share_playlist_with_list">Споделяне на списък с URL</string>
|
||||
<string name="delete_playback_states_alert">Изтрии всички позиции на възпроизвеждане?</string>
|
||||
<string name="watch_history_states_deleted">Позициите за възпроизвеждане са изтрити</string>
|
||||
@ -753,7 +750,7 @@
|
||||
<string name="enable_streams_notifications_title">Известия за нови потоци</string>
|
||||
<string name="any_network">Всяка мрежа</string>
|
||||
<string name="updates_setting_description">Покажи известие за актуализация на приложението, когато е налична нова версия</string>
|
||||
<string name="account_terminated">Акаунтът е прекратен</string>
|
||||
<string name="account_terminated">Профилът е прекратен</string>
|
||||
<string name="detail_pinned_comment_view_description">Фиксиран коментар</string>
|
||||
<string name="streams_not_yet_supported_removed">Потоци, които все още не се поддържат от програмата за изтегляне, не се показват</string>
|
||||
<string name="soundcloud_go_plus_content">Това е песен на SoundCloud Go+, поне във вашата страна, така че не може да бъде предавана поточно или изтеглена от NewPipe.</string>
|
||||
|
@ -728,4 +728,11 @@
|
||||
<string name="audio_track">Pista d\'àudio</string>
|
||||
<string name="no">No</string>
|
||||
<string name="no_streams">Cap emissió</string>
|
||||
<string name="enable_streams_notifications_summary">Notifica sobre les noves retransmissions de les subscripcions</string>
|
||||
<string name="enable_streams_notifications_title">Noves notificacions de retransmissions</string>
|
||||
<string name="duplicate_in_playlist">Les llistes de reproducció que estan en gris ja contenen aquest element.</string>
|
||||
<string name="unset_playlist_thumbnail">Desestableix la miniatura permanent</string>
|
||||
<string name="playlist_add_stream_success_duplicate">Duplicat afegit/s %d vegada/es</string>
|
||||
<string name="disable_media_tunneling_automatic_info">El túnel multimèdia s\'ha desactivat de manera predeterminada al dispositiu perquè se sap que el vostre model de dispositiu no ho permet.</string>
|
||||
<string name="semitone">Semiton</string>
|
||||
</resources>
|
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="upload_date_text">Publikováno na %1$s</string>
|
||||
<string name="upload_date_text">Publikováno %1$s</string>
|
||||
<string name="no_player_found">Nenalezen žádný přehrávač. Nainstalovat VLC?</string>
|
||||
<string name="install">Instalovat</string>
|
||||
<string name="cancel">Zrušit</string>
|
||||
@ -785,7 +785,7 @@
|
||||
<string name="question_mark">\?</string>
|
||||
<string name="metadata_subscribers">Odběratelé</string>
|
||||
<string name="show_channel_tabs_summary">Které karty mají být zobrazeny na stránkách kanálů</string>
|
||||
<string name="share_playlist_with_list">Sdílet URL seznamu</string>
|
||||
<string name="share_playlist_with_list">Sdílet seznam adres</string>
|
||||
<string name="share_playlist_with_titles">Sdílet s názvy</string>
|
||||
<string name="share_playlist_content_details">%1$s
|
||||
\n%2$s</string>
|
||||
@ -806,7 +806,6 @@
|
||||
<string name="channel_tab_albums">Alba</string>
|
||||
<string name="rewind">Přetočení zpět</string>
|
||||
<string name="replay">Znovu přehrát</string>
|
||||
<string name="share_playlist_with_titles_message">Sdílejte playlist s podrobnostmi jako je jeho název a názvy videí, nebo jako jednoduchý seznam adres videí</string>
|
||||
<string name="image_quality_medium">Střední kvalita</string>
|
||||
<string name="metadata_banners">Bannery</string>
|
||||
<string name="channel_tab_playlists">Playlisty</string>
|
||||
|
@ -772,7 +772,6 @@
|
||||
<string name="image_quality_none">Indlæs ikke billeder</string>
|
||||
<string name="image_quality_low">Lav kvalitet</string>
|
||||
<string name="share_playlist">Del Playliste</string>
|
||||
<string name="share_playlist_with_titles_message">Del playliste med detajler såsom playlistenavn og videotitler eller som en simpel liste over video-URL\'er</string>
|
||||
<string name="share_playlist_with_titles">Del med Titler</string>
|
||||
<string name="share_playlist_with_list">Del URL-liste</string>
|
||||
<plurals name="replies">
|
||||
@ -825,4 +824,5 @@
|
||||
\nEr du sikker på, at du vil fortsætte?</string>
|
||||
<string name="import_settings_vulnerable_format">Indstillingerne i den eksport, der importeres, bruger et sårbart format, der er blevet forældet siden NewPipe 0.27.0. Sørg for, at den eksport, der importeres, er fra en pålidelig kilde, og brug helst kun eksport fra NewPipe 0.27.0 eller nyere i fremtiden. Understøttelse af import af indstillinger i dette sårbare format fjernes snart helt, og så vil gamle versioner af NewPipe ikke længere være i stand til at importere indstillinger fra eksport fra nye versioner.</string>
|
||||
<string name="settings_category_backup_restore_title">Sikkerhedskopiering og gendannelse</string>
|
||||
<string name="audio_track_type_secondary">sekundær</string>
|
||||
</resources>
|
@ -63,7 +63,7 @@
|
||||
<string name="error_snackbar_message">Entschuldigung, etwas ist schiefgelaufen.</string>
|
||||
<string name="your_comment">Dein Kommentar (auf englisch):</string>
|
||||
<string name="duration_live">Live</string>
|
||||
<string name="main_bg_subtitle">Tippe auf die Lupe, um zu beginnen.</string>
|
||||
<string name="main_bg_subtitle">Tippe auf die Lupe, um zu suchen.</string>
|
||||
<string name="downloads">Downloads</string>
|
||||
<string name="downloads_title">Downloads</string>
|
||||
<string name="error_report_title">Fehlerbericht</string>
|
||||
@ -135,7 +135,7 @@
|
||||
<string name="action_history">Verlauf</string>
|
||||
<string name="subscription_change_failed">Abonnement konnte nicht geändert werden</string>
|
||||
<string name="subscription_update_failed">Abonnement konnte nicht aktualisiert werden</string>
|
||||
<string name="resume_on_audio_focus_gain_summary">Nach Unterbrechungen (z. B. Telefonaten) Wiedergabe fortsetzen</string>
|
||||
<string name="resume_on_audio_focus_gain_summary">Nach Unterbrechungen (z. B. Telefonaten) Wiedergabe fortsetzen</string>
|
||||
<string name="notification_channel_name">NewPipe-Benachrichtigung</string>
|
||||
<string name="notification_channel_description">Benachrichtigungen für den NewPipe-Player</string>
|
||||
<string name="settings_category_player_behavior_title">Verhalten</string>
|
||||
@ -319,7 +319,7 @@
|
||||
\nDu musst den Datenschutzrichtlinien zustimmen, um den Fehlerbericht an uns zu senden.</string>
|
||||
<string name="limit_data_usage_none_description">Unbegrenzt</string>
|
||||
<string name="limit_mobile_data_usage_title">Auflösung bei Verwendung mobiler Daten begrenzen</string>
|
||||
<string name="minimize_on_exit_title">Beim App-wechsel minimieren</string>
|
||||
<string name="minimize_on_exit_title">Beim App-Wechsel minimieren</string>
|
||||
<string name="minimize_on_exit_summary">Aktion beim Umschalten auf eine andere App vom Haupt-Videoplayer — %s</string>
|
||||
<string name="minimize_on_exit_none_description">Keine</string>
|
||||
<string name="minimize_on_exit_background_description">Für die Wiedergabe im Hintergrund minimieren</string>
|
||||
@ -409,7 +409,7 @@
|
||||
<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>
|
||||
<string name="enable_queue_limit_desc">Ein Download wird zur gleichen Zeit ausgeführt</string>
|
||||
<string name="enable_queue_limit_desc">Ein Download wird gleichzeitig ausgeführt</string>
|
||||
<string name="start_downloads">Downloads starten</string>
|
||||
<string name="pause_downloads">Downloads anhalten</string>
|
||||
<string name="downloads_storage_ask_title">Downloadziel abfragen</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 Zuschauende</item>
|
||||
<item quantity="other">%s Zuschauer</item>
|
||||
</plurals>
|
||||
<string name="no_one_listening">Niemand hört zu</string>
|
||||
<plurals name="listening">
|
||||
@ -497,7 +497,7 @@
|
||||
<string name="feed_oldest_subscription_update">Feed zuletzt aktualisiert: %s</string>
|
||||
<string name="feed_update_threshold_title">Grenzwert für Feed-Aktualisierung</string>
|
||||
<string name="feed_use_dedicated_fetch_method_title">Aus fest zugeordnetem Feed abrufen wenn verfügbar</string>
|
||||
<string name="feed_use_dedicated_fetch_method_summary">Steht in manchen Diensten zur Verfügung, ist meist viel schneller, liefert aber eventuell eine eingeschränkte Anzahl an Elementen und oft unvollständige Informationen (z. B. keine Videolänge, keinen Elementtyp, keinen Live-Status)</string>
|
||||
<string name="feed_use_dedicated_fetch_method_summary">Steht in manchen Diensten zur Verfügung, ist meist viel schneller, liefert aber eventuell eine eingeschränkte Anzahl an Elementen und oft unvollständige Informationen (z. B. keine Videolänge, keinen Elementtyp, keinen Live-Status)</string>
|
||||
<string name="feed_use_dedicated_fetch_method_help_text">Glaubst du, dass das Laden von Feeds zu langsam ist? Wenn ja, versuche den Schnelllademodus einzuschalten (du kannst ihn in den Einstellungen oder über die Schaltfläche unten ändern).
|
||||
\n
|
||||
\nNewPipe bietet zwei Feed-Ladestrategien:
|
||||
@ -527,7 +527,7 @@
|
||||
<string name="remove_watched_popup_title">Gesehene Videos entfernen\?</string>
|
||||
<string name="show_original_time_ago_title">Originalzeit vor Elementen anzeigen</string>
|
||||
<string name="show_original_time_ago_summary">Originaltexte von Diensten werden in Stream-Elementen sichtbar sein</string>
|
||||
<string name="youtube_restricted_mode_enabled_title">Aktivieren des „Eingeschränkten Modus“ von YouTube</string>
|
||||
<string name="youtube_restricted_mode_enabled_title">YouTubes „Eingeschränkten Modus“ aktivieren</string>
|
||||
<string name="detail_sub_channel_thumbnail_view_description">Profilbild des Kanals</string>
|
||||
<string name="channel_created_by">Erstellt von %s</string>
|
||||
<string name="video_detail_by">Von %s</string>
|
||||
@ -659,7 +659,7 @@
|
||||
<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>
|
||||
<string name="show_crash_the_player_title">\"Player abstürzen lassen\" anzeigen</string>
|
||||
<string name="show_crash_the_player_title">„Player abstürzen lassen“ anzeigen</string>
|
||||
<string name="crash_the_player">Player abstürzen lassen</string>
|
||||
<string name="show_crash_the_player_summary">Zeigt eine Absturzoption an, wenn der Player verwendet wird</string>
|
||||
<string name="error_report_channel_name">Fehlerbericht-Benachrichtigung</string>
|
||||
@ -719,7 +719,7 @@
|
||||
<string name="card">Karte</string>
|
||||
<string name="playlist_add_stream_success_duplicate">Duplikat %d mal hinzugefügt</string>
|
||||
<string name="duplicate_in_playlist">Die ausgegrauten Wiedergabelisten enthalten dieses Element bereits.</string>
|
||||
<string name="ignore_hardware_media_buttons_summary">Nützlich, wenn z. B. ein Headset mit defekten physischen Tasten verwendet wird</string>
|
||||
<string name="ignore_hardware_media_buttons_summary">Nützlich, wenn z. B. ein Headset mit defekten physischen Tasten verwendet wird</string>
|
||||
<string name="ignore_hardware_media_buttons_title">Ereignisse der Hardware-Medientasten ignorieren</string>
|
||||
<string name="remove_duplicates">Duplikate entfernen</string>
|
||||
<string name="remove_duplicates_title">Duplikate entfernen\?</string>
|
||||
@ -804,7 +804,6 @@
|
||||
<string name="share_playlist_content_details">%1$s
|
||||
\n%2$s</string>
|
||||
<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>
|
||||
|
@ -9,7 +9,7 @@
|
||||
<string name="download">Λήψη</string>
|
||||
<string name="search">Αναζήτηση</string>
|
||||
<string name="settings">Ρυθμίσεις</string>
|
||||
<string name="did_you_mean">Μήπως εννοείτε «%1$s»;</string>
|
||||
<string name="did_you_mean">Μήπως εννοείτε \"%1$s\";</string>
|
||||
<string name="share_dialog_title">Κοινοποίηση με</string>
|
||||
<string name="use_external_video_player_title">Χρήση εξωτερικής εφαρμογής αναπαραγωγής βίντεο</string>
|
||||
<string name="use_external_audio_player_title">Χρήση εξωτερικής συσκευής αναπαραγωγής ήχου</string>
|
||||
@ -103,7 +103,7 @@
|
||||
<string name="resume_on_audio_focus_gain_title">Ανάκτηση αναπαραγωγής</string>
|
||||
<string name="resume_on_audio_focus_gain_summary">Συνέχιση της αναπαραγωγής έπειτα από διακοπές (π.χ. κλήσεις)</string>
|
||||
<string name="show_hold_to_append_title">Εμφάνιση επεξήγησης του «Πιέστε παρατεταμένα για προσθήκη στην ουρά»</string>
|
||||
<string name="show_hold_to_append_summary">Εμφάνιση υπόδειξης όταν πατηθεί το κουμπί παρασκηνίου ή αναδυόμενου παραθύρου στη σελίδα λεπτομερειών του βίντεο</string>
|
||||
<string name="show_hold_to_append_summary">Εμφάνιση υπόδειξης όταν πατηθεί το κουμπί παρασκηνίου ή αναδυόμενου παραθύρου στις \"Λεπτομέρειες:\\ στο βίντεο</string>
|
||||
<string name="default_content_country_title">Προεπιλεγμένη χώρα περιεχομένου</string>
|
||||
<string name="settings_category_player_title">Αναπαραγωγός</string>
|
||||
<string name="settings_category_player_behavior_title">Συμπεριφορά</string>
|
||||
@ -472,7 +472,7 @@
|
||||
<string name="restricted_video">Αυτό το βίντεο έχει περιορισμό ηλικίας.
|
||||
\n
|
||||
\nΕνεργοποιήστε το «%1$s» στις ρυθμίσεις εάν θέλετε να το δείτε.</string>
|
||||
<string name="youtube_restricted_mode_enabled_title">Λειτουργία περιορισμένης πρόσβασης του YouTube</string>
|
||||
<string name="youtube_restricted_mode_enabled_title">Ενεργοποίηση \"Περιορισμένη Λειτουργία\\ του YouTube</string>
|
||||
<string name="unsupported_url_dialog_message">Δεν ήταν δυνατή η αναγνώριση της διεύθυνσης URL. Άνοιγμα με άλλη εφαρμογή;</string>
|
||||
<string name="auto_queue_toggle">Αυτόματη προσθήκη στην ουρά</string>
|
||||
<string name="clear_queue_confirmation_description">Η ουρά του ενεργού αναπαραγωγού θα αντικατασταθεί</string>
|
||||
@ -656,7 +656,7 @@
|
||||
<string name="manual_update_description">Χειροκίνητος έλεγχος για νέα έκδοση</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_title">Εμφάνιση «Κατάρρευση αναπαραγωγέα\\</string>
|
||||
<string name="show_crash_the_player_summary">Εμφανίζει μια επιλογή κατάρρευσης κατά τη χρήση του αναπαραγωγέα</string>
|
||||
<string name="crash_the_player">Κατάρρευση αναπαραγωγέα</string>
|
||||
<string name="error_report_channel_name">Ειδοποίηση αναφοράς σφάλματος</string>
|
||||
@ -804,7 +804,6 @@
|
||||
<string name="share_playlist_content_details">%1$s
|
||||
\n%2$s</string>
|
||||
<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>
|
||||
|
@ -12,11 +12,11 @@
|
||||
<string name="share_dialog_title">Konigi kun</string>
|
||||
<string name="use_external_video_player_title">Uzi eksteran filmetoludilon</string>
|
||||
<string name="use_external_audio_player_title">Uzi eksteran sonludilon</string>
|
||||
<string name="default_resolution_title">Defaŭlta rezolucio</string>
|
||||
<string name="default_resolution_title">Preferata distingumo</string>
|
||||
<string name="play_with_kodi_title">Ludi per Kodi</string>
|
||||
<string name="show_play_with_kodi_title">Montri \"Ludi per Kodi\"-opcion</string>
|
||||
<string name="play_audio">Sono</string>
|
||||
<string name="default_audio_format_title">Defaŭlta sondosierformo</string>
|
||||
<string name="default_audio_format_title">Preferata sona dosierformo</string>
|
||||
<string name="theme_title">Etoso</string>
|
||||
<string name="dark_theme_title">Malhela</string>
|
||||
<string name="light_theme_title">Hela</string>
|
||||
@ -24,7 +24,7 @@
|
||||
<string name="download_dialog_title">Elŝuti</string>
|
||||
<string name="unsupported_url">Ligilo ne subtenita</string>
|
||||
<string name="content_language_title">Preferata enhavlingvo</string>
|
||||
<string name="settings_category_video_audio_title">Filmeto kaj sono</string>
|
||||
<string name="settings_category_video_audio_title">Filmo kaj sono</string>
|
||||
<string name="settings_category_appearance_title">Apero</string>
|
||||
<string name="background_player_playing_toast">Ludanta fone</string>
|
||||
<string name="general_error">Eraro</string>
|
||||
@ -32,17 +32,17 @@
|
||||
<string name="content_not_available">Enhavo malhavebla</string>
|
||||
<string name="detail_likes_img_view_description">Ŝatoj</string>
|
||||
<string name="detail_dislikes_img_view_description">Malŝatoj</string>
|
||||
<string name="no_player_found">Neniu elsendlflua ludilo trovita. Ĉu instali la aplikaĵon VLC\?</string>
|
||||
<string name="kore_not_found">Ĉu instali la mankan aplikaĵon Kore\?</string>
|
||||
<string name="show_next_and_similar_title">Montri \'Sekvajn\' kaj \'Similajn\' videojn</string>
|
||||
<string name="no_player_found">Neniu ludilo de elsendoj trovita. Ĉu instalu la programon VLC?</string>
|
||||
<string name="kore_not_found">Ĉu instalu la mankatan programon Kore?</string>
|
||||
<string name="show_next_and_similar_title">Montri ‹Sekvajn› kaj ‹Similajn› filmetojn</string>
|
||||
<string name="could_not_load_thumbnails">Ĉiuj bildetoj ne ŝargeblas</string>
|
||||
<string name="parsing_error">La retejo ne analizeblas</string>
|
||||
<string name="detail_thumbnail_view_description">Ludi filmeton, daŭro:</string>
|
||||
<string name="detail_uploader_thumbnail_view_description">Bildeto de la alŝutinto</string>
|
||||
<string name="download_path_title">Elŝutujo por filmetoj</string>
|
||||
<string name="download_path_audio_title">Elŝutujo por aŭdio</string>
|
||||
<string name="show_play_with_kodi_summary">Montri opcion por ludi filmeton per la aplikaĵo Kodi</string>
|
||||
<string name="download_path_summary">Elŝutitaj filmetoj estas konservitaj tie</string>
|
||||
<string name="show_play_with_kodi_summary">Montri opcion por ludi filmeton per la programo Kodi</string>
|
||||
<string name="download_path_summary">Elŝutitaj filmetoj konserviĝas ĉi tie</string>
|
||||
<string name="download_path_audio_summary">Dosierujo por konservi elŝutitajn muzikojn</string>
|
||||
<string name="download_path_dialog_title">Elektu lokon por konservi elŝutitajn filmetojn</string>
|
||||
<string name="download_path_audio_dialog_title">Elektu lokon por konservi elŝutitajn muzikojn</string>
|
||||
@ -57,7 +57,7 @@
|
||||
<string name="main_bg_subtitle">Premi \"Serĉi\" por komenci.</string>
|
||||
<string name="no_player_found_toast">Neniu elsendlflua ludilo trovita (instalu VLC por ludi ĝin).</string>
|
||||
<string name="open_in_popup_mode">Malfermi en ŝprucfenestran modon</string>
|
||||
<string name="use_external_video_player_summary">Forigas aŭdon ĉe kelkaj rezolucioj</string>
|
||||
<string name="use_external_video_player_summary">Forigas aŭdon ĉe kelkaj distingumoj</string>
|
||||
<string name="subscribe_button_title">Aboni</string>
|
||||
<string name="subscribed_button_title">Abonita</string>
|
||||
<string name="channel_unsubscribed">Kanalo malabonita</string>
|
||||
@ -71,10 +71,10 @@
|
||||
<string name="controls_background_title">Fono</string>
|
||||
<string name="controls_popup_title">Ŝprucfenestro</string>
|
||||
<string name="controls_add_to_playlist_title">Aldonu al</string>
|
||||
<string name="default_popup_resolution_title">Defaŭlta rezolucio de la ŝprucfenestra ludilo</string>
|
||||
<string name="show_higher_resolutions_title">Montri pli altajn rezoluciojn</string>
|
||||
<string name="show_higher_resolutions_summary">Nur kelkaj aparatoj povas ludi 2K / 4K filmetojn</string>
|
||||
<string name="default_video_format_title">Defaŭlta filmetdosierformo</string>
|
||||
<string name="default_popup_resolution_title">Komenca distingumo de la ŝprucfenestra ludilo</string>
|
||||
<string name="show_higher_resolutions_title">Montri pli altajn distingumojn</string>
|
||||
<string name="show_higher_resolutions_summary">Nur kelkaj aparatoj povas ludi filmetojn je distingumoj 2K / 4K</string>
|
||||
<string name="default_video_format_title">Preferata filma dosierformo</string>
|
||||
<string name="popup_remember_size_pos_title">Memori ecojn de ŝprucfenestro</string>
|
||||
<string name="popup_remember_size_pos_summary">Memori lastan grandon kaj pozicion de ŝprucfenestro</string>
|
||||
<string name="use_inexact_seek_title">Uzi rapidan malekzaktan serĉon</string>
|
||||
@ -100,7 +100,7 @@
|
||||
<string name="clear">Forviŝi</string>
|
||||
<string name="show_search_suggestions_title">Serĉi sugestojn</string>
|
||||
<string name="show_search_suggestions_summary">Montri sugestojn kiam serĉanto</string>
|
||||
<string name="best_resolution">Plej bona rezolucio</string>
|
||||
<string name="best_resolution">Plej bona distingumo</string>
|
||||
<string name="app_description">Libera malpeza torentado ĉe Android.</string>
|
||||
<string name="settings_category_downloads_title">Elŝuti</string>
|
||||
<string name="charset_letters_and_digits">Leteroj kaj ciferoj</string>
|
||||
@ -110,7 +110,7 @@
|
||||
<string name="enable_search_history_title">Serĉa historio</string>
|
||||
<string name="enable_search_history_summary">Konservi la historio de serĉo lokale</string>
|
||||
<string name="enable_watch_history_title">Rigardu historion</string>
|
||||
<string name="enable_watch_history_summary">Spuri la viditajn filmetojn</string>
|
||||
<string name="enable_watch_history_summary">Protokoli spektitajn filmetojn</string>
|
||||
<string name="notification_channel_name">Sciigo de NewPipe</string>
|
||||
<string name="notification_channel_description">Sciigoj por ludilo de NewPipe</string>
|
||||
<string name="settings_category_player_title">Ludilo</string>
|
||||
@ -121,7 +121,7 @@
|
||||
<string name="top_50">Supro 50</string>
|
||||
<string name="new_and_hot">Nova kaj varma</string>
|
||||
<string name="show_hold_to_append_title">Montri la indiko « Tenu por aldoni »</string>
|
||||
<string name="show_hold_to_append_summary">Montri indikon premante la fona aŭ la ŝprucfenestra butono en filmeta \"Detaloj:\"</string>
|
||||
<string name="show_hold_to_append_summary">Montri indikon premante la fonon aŭ la ŝprucfenestran butonon en filmeta «Detaloj:»</string>
|
||||
<string name="play_all">Ludi ĉiujn</string>
|
||||
<string name="player_stream_failure">Ne povis ludi tion torenton</string>
|
||||
<string name="player_unrecoverable_failure">Neatendebla eraro de ludilo okazis</string>
|
||||
@ -138,7 +138,7 @@
|
||||
<string name="switch_to_main">Ŝangi al Ĉefa</string>
|
||||
<string name="always">Ĉiam</string>
|
||||
<string name="just_once">Nur unufoje</string>
|
||||
<string name="video_streams_empty">Neniuj filmeta torentoj trovitaj</string>
|
||||
<string name="video_streams_empty">Neniu filmofluo trovita</string>
|
||||
<string name="audio_streams_empty">Neniuj sonaj torentoj trovis</string>
|
||||
<string name="popup_player">Ŝprucfenestra ludilo</string>
|
||||
<string name="import_data_title">Importi la datumbazon</string>
|
||||
@ -191,7 +191,7 @@
|
||||
<string name="clear_search_history_summary">Forviŝi la serĉajn ŝlosilvortojn</string>
|
||||
<string name="delete_search_history_alert">Ĉu vi volas forviŝi la totalon de la historio de serĉo \?</string>
|
||||
<string name="search_history_deleted">Historio de serĉo forviŝita</string>
|
||||
<string name="limit_mobile_data_usage_title">Limigi rezolucio kiam uzanta moveblan datumon</string>
|
||||
<string name="limit_mobile_data_usage_title">Limigi distingumon uzante telefonan retkonekton</string>
|
||||
<string name="minimize_on_exit_popup_description">Minimumigi al ŝprucfenestra ludilo</string>
|
||||
<string name="channels">Kanaloj</string>
|
||||
<string name="playlists">Ludlistoj</string>
|
||||
@ -258,8 +258,8 @@
|
||||
<string name="msg_copied">Enpoŝigita</string>
|
||||
<string name="no_available_dir">Bonvolu difini elŝutan dosierujon poste en agordoj</string>
|
||||
<plurals name="views">
|
||||
<item quantity="one">%s spektaĵo</item>
|
||||
<item quantity="other">%s spektaĵoj</item>
|
||||
<item quantity="one">%s spekto</item>
|
||||
<item quantity="other">%s spektoj</item>
|
||||
</plurals>
|
||||
<string name="short_thousand">k</string>
|
||||
<string name="short_million">M</string>
|
||||
@ -286,7 +286,7 @@
|
||||
<item quantity="one">%s abonanto</item>
|
||||
<item quantity="other">%s abonantoj</item>
|
||||
</plurals>
|
||||
<string name="no_views">Neniuj spektaĵoj</string>
|
||||
<string name="no_views">Neniuj spektoj</string>
|
||||
<string name="no_videos">Neniu filmeto</string>
|
||||
<string name="delete_item_search_history">Ĉu vi volas forviŝi tion eron el la historio de serĉo \?</string>
|
||||
<string name="main_page_content">Enhavo de la ĉefpaĝo</string>
|
||||
@ -305,7 +305,7 @@
|
||||
<string name="drawer_open">Malfermi la tirkeston</string>
|
||||
<string name="drawer_close">Fermi la tirtekston</string>
|
||||
<string name="external_player_unsupported_link_type">Ekstaraj ludantoj ne suportas tiajn ligilojn</string>
|
||||
<string name="video_player">Filmetoludilo</string>
|
||||
<string name="video_player">Filmludilo</string>
|
||||
<string name="background_player">Fona ludilo</string>
|
||||
<string name="preferred_player_fetcher_notification_title">Akiranta informoj…</string>
|
||||
<string name="preferred_player_fetcher_notification_message">Ŝarĝante petita enhavo</string>
|
||||
@ -347,7 +347,7 @@
|
||||
<string name="playback_pitch">Ludkampo</string>
|
||||
<string name="unhook_checkbox">Malligi (povas kaŭzi distordon)</string>
|
||||
<string name="preferred_open_action_settings_title">Preferita \'malfermi\' ago</string>
|
||||
<string name="preferred_open_action_settings_summary">Defaŭlta ago malfermante enhavo — %s</string>
|
||||
<string name="preferred_open_action_settings_summary">Implicita ago malfermante vidaŭdaĵon — %s</string>
|
||||
<string name="caption_setting_title">Subtitoloj</string>
|
||||
<string name="caption_setting_description">Modifi la dimension de la teksto kaj la fonajn stilojn de la subtitoloj de la ludilo. Ĝi bezonas restarto de la apo por efektiviĝi.</string>
|
||||
<string name="one_item_deleted">1 ero forviŝita.</string>
|
||||
@ -369,9 +369,9 @@
|
||||
<string name="skip_silence_checkbox">Plirapidigi dum silentoj</string>
|
||||
<string name="playback_step">Paŝo</string>
|
||||
<string name="playback_reset">Restarigi</string>
|
||||
<string name="saved_tabs_invalid_json">Ne povis legi konservitajn ongletoj, tial uzante la defaŭltajn</string>
|
||||
<string name="restore_defaults">Restaŭri la defaŭltojn</string>
|
||||
<string name="restore_defaults_confirmation">Ĉu vi volas restaŭri la defaŭltojn valorojn\?</string>
|
||||
<string name="saved_tabs_invalid_json">Ne povis legi agorditajn langetojn, do uzos la implicitajn</string>
|
||||
<string name="restore_defaults">Restarigi implicitajn valorojn</string>
|
||||
<string name="restore_defaults_confirmation">Ĉu vi volas restarigi la implicitajn valorojn?</string>
|
||||
<string name="subscribers_count_not_available">Abonantoj kalkulo malhaveblas</string>
|
||||
<string name="main_page_content_summary">Kioj ongletoj estas montritaj en la ĉefpaĝo</string>
|
||||
<string name="updates_setting_title">Ĝisdatigoj</string>
|
||||
@ -410,7 +410,7 @@
|
||||
<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>
|
||||
<string name="default_kiosk_page_summary">Defaŭlta Kiosko</string>
|
||||
<string name="default_kiosk_page_summary">Implicita Kiosko</string>
|
||||
<string name="no_one_watching">Neniu spektas</string>
|
||||
<plurals name="watching">
|
||||
<item quantity="one">%s spektanto</item>
|
||||
@ -443,7 +443,7 @@
|
||||
<string name="videos_string">Filmetoj</string>
|
||||
<string name="permission_display_over_apps">Doni la permeson por afiŝiĝi supre aliaj apoj</string>
|
||||
<string name="app_language_title">Preferata aplingvo</string>
|
||||
<string name="systems_language">Sistemnormo</string>
|
||||
<string name="systems_language">Sistema</string>
|
||||
<string name="subtitle_activity_recaptcha">Premu “Finita” kiam solvita</string>
|
||||
<string name="done">Finita</string>
|
||||
<plurals name="seconds">
|
||||
@ -499,9 +499,7 @@
|
||||
<string name="notification_action_repeat">Ripeti</string>
|
||||
<string name="search_showing_result_for">Montrante rezultojn pri: %s</string>
|
||||
<string name="open_with">Malfermi per</string>
|
||||
<string name="restricted_video">Tiu ĉi filmeto havas aĝlimon.
|
||||
\n
|
||||
\nŜalti \"%1$s\" en la agordoj, se vi volas vidi ĝin.</string>
|
||||
<string name="restricted_video">Tiu ĉi filmeto havas aĝminimumon. \n \nŜaltu «%1$s» en la agordoj, se vi volus vidi ĝin.</string>
|
||||
<string name="night_theme_title">Malhela etoso</string>
|
||||
<string name="notification_colorize_title">farbi sciigon</string>
|
||||
<string name="notification_action_buffering">Alŝuto</string>
|
||||
@ -530,11 +528,11 @@
|
||||
<string name="description_tab_description">Priskribo</string>
|
||||
<string name="comments_tab_description">Komentoj</string>
|
||||
<string name="show_description_title">Montri priskribon</string>
|
||||
<string name="notification_scale_to_square_image_summary">Stuci la bildeton de la video en la sciigo de 16:9 ĝis 1:1 propocio</string>
|
||||
<string name="notification_scale_to_square_image_summary">Stuci la antaŭvidan bildeton de la filmo en la sciigo de proporcio 16:9 al 1:1</string>
|
||||
<string name="show_meta_info_title">Montri metadatumojn</string>
|
||||
<string name="mark_as_watched">Marki spektita</string>
|
||||
<string name="show_meta_info_summary">Malŝatu por kaŝi metadatumujojn kio havas aldonajn informojn pri la elsendfluisto, enhavo de la fluo, aŭ serĉpto</string>
|
||||
<string name="show_description_summary">Malŝaltu por kaŝi la videan priskribon kaj aldonan informon</string>
|
||||
<string name="show_description_summary">Malŝaltu por kaŝi priskribojn kaj aldonajn informojn de filmetoj</string>
|
||||
<string name="clear_queue_confirmation_title">Peti por konfirmo antaŭ vakigado atendvico</string>
|
||||
<string name="clear_queue_confirmation_summary">Via atendvico povas anstataŭigi se vi ŝanĝi al malsama ludilo</string>
|
||||
<string name="clear_queue_confirmation_description">La aktiva ludila atendvico anstataŭigos</string>
|
||||
@ -549,7 +547,7 @@
|
||||
<string name="volume">Laŭteco</string>
|
||||
<string name="none">Neniu</string>
|
||||
<string name="notification_colorize_summary">Permesi al Android agordi koloron de sciigo laŭ la precipa koloro de videaĵminiaturo (noti, ke ĉi tio ne disponeblas en ĉiuj iloj)</string>
|
||||
<string name="auto_queue_toggle">Aŭtomata vicigado</string>
|
||||
<string name="auto_queue_toggle">Memaga vicigado</string>
|
||||
<string name="right_gesture_control_title">Ago de dekstra gesto</string>
|
||||
<string name="notification_actions_summary">Redakti ĉiun agon de sciigo per tuŝi gin. Elekti maksimume tri agon por montri en la kompakta sciigo per markobutonoj dekstre.</string>
|
||||
<string name="prefer_original_audio_summary">Elekti la originalan aŭdiotrakon malgraŭ lingvo</string>
|
||||
@ -574,20 +572,19 @@
|
||||
<string name="remote_search_suggestions">Foraj serĉsugestoj</string>
|
||||
<string name="error_report_channel_description">Sciigoj por raporti erarojn</string>
|
||||
<string name="loading_metadata_title">Ŝargante metadatumoj…</string>
|
||||
<string name="hash_channel_description">Sciigo por kreado de haketaĵoj de videoj</string>
|
||||
<string name="hash_channel_description">Sciigo por kreado de haketaĵoj de filmetoj</string>
|
||||
<string name="youtube_restricted_mode_enabled_summary">YouTube provizas \"Limigitan Reĝimon\", kiu kaŝas enhavon, kiu potence maltaŭgas por infanoj</string>
|
||||
<string name="restricted_video_no_stream">Ĉi tiu video estas aĝo-limigita.
|
||||
\nPro novaj reguloj de YouTube, kiuj aplikas al aĝo-limigitaj videoj, NewPipe ne povas atingi iun ajn video-fluoj de ĉi tiu video kaj konsekvence ne povas ludi ĝin.</string>
|
||||
<string name="restricted_video_no_stream">Ĉi tiu filmeto havas aĝminimumon. \nPro novaj reguloj de YouTube, kiuj rilatas filmojn kun minimuma aĝo, NewPipe ne povas atingi ajnan fluon de ĉi tiu filmo kaj tial ne povas ludi ĝin.</string>
|
||||
<string name="error_report_channel_name">Sciigo por erar-raportoj</string>
|
||||
<string name="notifications">Sciigoj</string>
|
||||
<string name="settings_category_player_notification_title">Ludila sciigo</string>
|
||||
<string name="hash_channel_name">Sciigo por haketado de videoj</string>
|
||||
<string name="hash_channel_name">Sciigo por haketado de filmetoj</string>
|
||||
<string name="local_search_suggestions">Lokaj serĉsugestoj</string>
|
||||
<string name="start_main_player_fullscreen_title">Ŝalti ĉefan ludilon plenekrane</string>
|
||||
<string name="streams_notification_channel_description">Sciigo por novaj fluoj de abonoj</string>
|
||||
<string name="clear_cookie_title">Forigi kuketojn de reCAPTCHA</string>
|
||||
<string name="settings_category_player_notification_summary">Agordi la sciigon por ĉi-momente ludantaj datumtorentoj</string>
|
||||
<string name="start_main_player_fullscreen_summary">Ne komenci ludi videojn en la mini-ludilo, sed ŝalti plenekranan reĝimon rekte, se aŭtomata rotacio ŝlositas. Vi ankoraŭ povus atingi mini-ludilon, se vi elirus plenekranan reĝimon.</string>
|
||||
<string name="start_main_player_fullscreen_summary">Ne ekludu filmojn en la etludilo, sed ŝalti plenekranan reĝimon rekte, se memaga turniĝo estas ŝaltita. Vi ankoraŭ povus aliri la etludilon elirinte la plenekranan reĝimon.</string>
|
||||
<string name="clear_cookie_summary">Forigi kuketojn, kiujn NewPipe konservas, kiam vi solvas reCAPTCHA-taskojn</string>
|
||||
<string name="error_report_notification_title">NewPipe renkontis eraron, tuŝi por raporti</string>
|
||||
<string name="main_tabs_position_title">Pozicio de la ĉefaj langetoj</string>
|
||||
@ -605,7 +602,7 @@
|
||||
<string name="import_subscriptions_hint">Importi aŭ eksporti abonojn per la tri-punkta menuo</string>
|
||||
<string name="msg_calculating_hash">Kalkulado de haketaĵo</string>
|
||||
<string name="faq_title">Oftaj demandoj</string>
|
||||
<string name="no_dir_yet">Neniu dosierujo por elŝutoj agordita, bonvolu elekti la defaŭltan elŝuto-dosierujon nun</string>
|
||||
<string name="no_dir_yet">Neniu dosierujo por elŝutoj agordita, bonvolu elekti la preferatan elŝuto-dosierujon nun</string>
|
||||
<string name="error_report_open_github_notice">Bonvolu certigi, ĉu erarraporto, kiu diskutas pri via eraro, jam ekzistas. Kreado de duoblaĵaj erarraportoj forprenas tempon el ni, kiun ni povus uzi por ripari la veran eraron.</string>
|
||||
<string name="related_items_tab_description">Rilatajn erojn</string>
|
||||
<string name="recaptcha_solve">Solvi</string>
|
||||
@ -613,4 +610,12 @@
|
||||
<string name="downloads_storage_ask_summary_no_saf_notice">Oni petos al vi kien salvi ĉiujn elŝutojn</string>
|
||||
<string name="yes">Jes</string>
|
||||
<string name="no">Ne</string>
|
||||
<string name="remove_watched_popup_title">Ĉu forigu spektitajn filmetojn?</string>
|
||||
<string name="youtube_music_premium_content">Ĉu tiu filmeto sole haveblas al abonantoj de YouTube Music Premium, do ĝi ne spekteblas nek elŝuteblas de NewPipe.</string>
|
||||
<string name="audio_track_present_in_video">Sono devus jam esti en ĉi tiu fluo</string>
|
||||
<string name="no_video_streams_available_for_external_players">Neniu filmofluo ludeblas por ekstera ludilo</string>
|
||||
<string name="channel_tab_videos">Filmetoj</string>
|
||||
<string name="remove_watched_popup_warning">Filmetoj kiuj spektiĝis antaŭ aŭ post sia aldoniĝo al la ludlisto foriĝus.. \nĈu vi certas? Ĉi tio nemalfareblus!</string>
|
||||
<string name="reset_settings_summary">Restarigi implicitajn agordojn</string>
|
||||
<string name="remove_watched_popup_yes_and_partially_watched_videos">Jes, kaj ankaŭ parte spektitajn filmetojn</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">Toca la lupa para empezar.</string>
|
||||
<string name="main_bg_subtitle">Toca la lupa para comenzar..</string>
|
||||
<string name="duration_live">En directo</string>
|
||||
<string name="downloads">Descargas</string>
|
||||
<string name="downloads_title">Descargas</string>
|
||||
@ -818,7 +818,6 @@
|
||||
<string name="share_playlist_content_details">%1$s
|
||||
\n%2$s</string>
|
||||
<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>
|
||||
|
@ -89,7 +89,7 @@
|
||||
<string name="duration_live">Otse</string>
|
||||
<string name="downloads">Allalaadimised</string>
|
||||
<string name="downloads_title">Allalaadimised</string>
|
||||
<string name="error_report_title">Vea teatamine</string>
|
||||
<string name="error_report_title">Veateade</string>
|
||||
<string name="all">Kõik</string>
|
||||
<string name="disabled">Keelatud</string>
|
||||
<string name="clear">Kustuta</string>
|
||||
@ -99,8 +99,8 @@
|
||||
<string name="always">Alati</string>
|
||||
<string name="just_once">Üks kord</string>
|
||||
<string name="file">Fail</string>
|
||||
<string name="notification_channel_name">NewPipe teavitus</string>
|
||||
<string name="notification_channel_description">Teavitused NewPipe pleierile</string>
|
||||
<string name="notification_channel_name">NewPipe\'i teavitus</string>
|
||||
<string name="notification_channel_description">NewPipe\'i meediaesitaja teavitused</string>
|
||||
<string name="unknown_content">[Tundmatu]</string>
|
||||
<string name="switch_to_background">Lülita taustale</string>
|
||||
<string name="switch_to_popup">Lülita hüpikpleierile</string>
|
||||
@ -180,10 +180,10 @@
|
||||
<string name="dismiss">Loobu</string>
|
||||
<string name="rename">Muuda nime</string>
|
||||
<string name="ok">OK</string>
|
||||
<string name="msg_name">Faili nimi</string>
|
||||
<string name="msg_name">Failinimi</string>
|
||||
<string name="msg_threads">Lõimed</string>
|
||||
<string name="msg_error">Viga</string>
|
||||
<string name="msg_running">NewPipe allalaadimine</string>
|
||||
<string name="msg_running">NewPipe\'i on allalaadimisel</string>
|
||||
<string name="msg_running_detail">Üksikasjade nägemiseks toksa</string>
|
||||
<string name="msg_wait">Palun oota…</string>
|
||||
<string name="msg_copied">Kopeeriti lõikepuhvrisse</string>
|
||||
@ -197,18 +197,18 @@
|
||||
<string name="settings_file_replacement_character_title">Asendustähemärk</string>
|
||||
<string name="charset_letters_and_digits">Tähed ja numbrid</string>
|
||||
<string name="charset_most_special_characters">Erimärgid</string>
|
||||
<string name="title_activity_about">NewPipe rakendusest</string>
|
||||
<string name="title_activity_about">Rakenduse teave: NewPipe</string>
|
||||
<string name="title_licenses">Kolmanda osapoole litsentsid</string>
|
||||
<string name="tab_about">Rakenduse teave ja KKK</string>
|
||||
<string name="tab_licenses">Litsentsid</string>
|
||||
<string name="contribution_title">Panusta</string>
|
||||
<string name="view_on_github">Vaata GitHubis</string>
|
||||
<string name="donation_title">Anneta</string>
|
||||
<string name="website_title">Veebileht</string>
|
||||
<string name="website_encouragement">Enama info saamiseks külasta NewPipe veebilehte.</string>
|
||||
<string name="privacy_policy_title">NewPipe privaatsuspoliitika</string>
|
||||
<string name="website_title">Veebisait</string>
|
||||
<string name="website_encouragement">Täiendava info ja uudiste lugemiseks külasta NewPipe\'i veebisaiti.</string>
|
||||
<string name="privacy_policy_title">NewPipe\'i privaatsuspoliitika</string>
|
||||
<string name="read_privacy_policy">Loe privaatsuspoliitikat</string>
|
||||
<string name="app_license_title">NewPipe litsents</string>
|
||||
<string name="app_license_title">NewPipe\'i litsents</string>
|
||||
<string name="read_full_license">Loe litsentsi</string>
|
||||
<string name="title_activity_history">Ajalugu</string>
|
||||
<string name="action_history">Ajalugu</string>
|
||||
@ -303,11 +303,10 @@
|
||||
<string name="copyright" formatted="true">© %1$s %2$s %3$s alla</string>
|
||||
<string name="app_description">Vaba ja lihtne voogesitus Androidis.</string>
|
||||
<string name="contribution_encouragement">Kui sul on ideid kujunduse muutmisest, koodi puhastamisest või suurtest koodi muudatustest - abi on alati teretulnud. Mida rohkem tehtud, seda paremaks läheb!</string>
|
||||
<string name="donation_encouragement">NewPipe arendajad on vabatahtlikud, kes kulutavad oma vaba aega, toomaks sulle parimat kasutamise kogemust. On aeg anda tagasi aidates arendajaid ja muuta NewPipe veel paremaks, nautides ise tassi kohvi.</string>
|
||||
<string name="donation_encouragement">NewPipe\'i arendajad on vabatahtlikud, kes kulutavad oma vaba aega, toomaks sulle parimat kasutuskogemust. On aeg anda tagasi aidates arendajaid ja muuta NewPipe veel paremaks, nautides ise tassi kohvi.</string>
|
||||
<string name="give_back">Anneta</string>
|
||||
<string name="privacy_policy_encouragement">NewPipe võtab privaatsust väga tõsiselt. Seetõttu ei kogu rakendus ilma nõusolekuta mingeid andmeid.
|
||||
\nNewPipe privaatsuspoliitika selgitab üksikasjalikult, milliseid andmeid saadetakse ja kogutakse veateate saatmisel.</string>
|
||||
<string name="app_license">NewPipe vaba avatud koodiga tarkvara. Seada võid kasutada, uurida, jagada ja parandada nii, nagu õigemaks pead. Täpsemalt - seda võid levitada ja/või muuta vastavalt Vaba Tarkvara Sihtasutuse avaldatud GNU Üldise Avaliku Litsentsi v.3 (või sinu valikul hilisema versiooni) tingimustele.</string>
|
||||
<string name="privacy_policy_encouragement">NewPipe võtab privaatsust väga tõsiselt. Seetõttu ei kogu rakendus ilma nõusolekuta mingeid andmeid. \nNewPipe\'i privaatsuspoliitika selgitab üksikasjalikult, milliseid andmeid saadetakse ja kogutakse veateate saatmisel.</string>
|
||||
<string name="app_license">NewPipe on vaba ja avatud lähtekoodiga tarkvara. Seada võid kasutada, uurida, jagada ja parandada nii, nagu õigemaks pead. Täpsemalt - seda võid levitada ja/või muuta vastavalt Vaba Tarkvara Sihtasutuse avaldatud GNU Üldise Avaliku Litsentsi v.3 (või sinu valikul hilisema versiooni) tingimustele.</string>
|
||||
<string name="enable_disposed_exceptions_title">Teavita elutsüklist väljas vigadest</string>
|
||||
<string name="import_soundcloud_instructions">Impordi SoundCloudi profiil trükkides URL või oma ID:
|
||||
\n
|
||||
@ -321,8 +320,7 @@
|
||||
<string name="skip_silence_checkbox">Keri helitu koht edasi</string>
|
||||
<string name="playback_step">Samm</string>
|
||||
<string name="playback_reset">Lähtesta</string>
|
||||
<string name="start_accept_privacy_policy">Selleks, et täita Euroopa Üldist Andmekaitse Määrust (GDPR), juhime tähelepanu NewPipe\'i privaatsuspoliitikale. Palun lugege seda hoolikalt.
|
||||
\nMeile veateate saatmiseks pead sellega nõustuma.</string>
|
||||
<string name="start_accept_privacy_policy">Selleks, et täita Euroopa Üldist Andmekaitse Määrust (GDPR), juhime tähelepanu NewPipe\'i privaatsuspoliitikale. Palun loe seda hoolikalt. \nMeile veateate saatmiseks pead sellega nõustuma.</string>
|
||||
<string name="minimize_on_exit_title">Minimeeri, kui kasutad teisi rakendusi</string>
|
||||
<string name="minimize_on_exit_summary">Tegevus lülitusel peamiselt videopleierilt teisele rakendusele — %s</string>
|
||||
<string name="minimize_on_exit_none_description">Pole</string>
|
||||
@ -335,7 +333,7 @@
|
||||
<string name="events">Sündmused</string>
|
||||
<string name="file_deleted">Fail kustutati</string>
|
||||
<string name="app_update_notification_channel_name">Rakenduse värskenduse teavitus</string>
|
||||
<string name="app_update_notification_channel_description">Teavitus NewPipe uuetest versioonidest</string>
|
||||
<string name="app_update_notification_channel_description">Teavitus NewPipe\'i uuetest versioonidest</string>
|
||||
<string name="download_to_sdcard_error_title">Väline andmekandja pole saadaval</string>
|
||||
<string name="download_to_sdcard_error_message">Allalaadimine välisele SD-kaardile ei ole võimalik. Kas lähtestada allalaadimiste kataloogi asukoht\?</string>
|
||||
<string name="saved_tabs_invalid_json">Tõrge salvestatud vahekaaride lugemisel; kasutatakse vaikeväärtusi</string>
|
||||
@ -350,7 +348,7 @@
|
||||
<string name="list">Nimekiri</string>
|
||||
<string name="grid">Võrgustik</string>
|
||||
<string name="auto">Auto</string>
|
||||
<string name="app_update_available_notification_title">NewPipe värskendus on saadaval!</string>
|
||||
<string name="app_update_available_notification_title">NewPipe\'i värskendus on saadaval!</string>
|
||||
<string name="missions_header_finished">Lõpetatud</string>
|
||||
<string name="missions_header_pending">Ootel</string>
|
||||
<string name="paused">peatatud</string>
|
||||
@ -369,7 +367,7 @@
|
||||
<string name="error_file_creation">Faili ei saa luua</string>
|
||||
<string name="error_ssl_exception">Turvalist ühendust ei suudetud luua</string>
|
||||
<string name="error_unknown_host">Serverit ei leitud</string>
|
||||
<string name="error_connect_host">Serveriga ei saadud ühendust</string>
|
||||
<string name="error_connect_host">Serveriga ei saa ühendust</string>
|
||||
<string name="error_http_no_content">Server ei saada andmeid</string>
|
||||
<string name="error_http_unsupported_range">Server ei toeta mitmelõimelisi allalaadimisi. Proovi uuesti kasutades @string/msg_threads = 1</string>
|
||||
<string name="error_http_not_found">Ei leitud</string>
|
||||
@ -556,7 +554,7 @@
|
||||
<string name="enable_queue_limit_desc">Luba korraga vaid üks allalaadimine</string>
|
||||
<string name="enable_queue_limit">Piira allalaadimiste järjekorda</string>
|
||||
<string name="error_progress_lost">Faili kustutamisega läks ka tööjärg kautsi</string>
|
||||
<string name="error_postprocessing_stopped">Faili töötlemisel NewPipe lõpetas töö</string>
|
||||
<string name="error_postprocessing_stopped">NewPipe lõpetas faili töötlemisel töö</string>
|
||||
<string name="disable_media_tunneling_summary">Lülita meedia tunneldamine välja juhul, kui esitamisel tekib must ekraan või pildi kuvamine on katkendlik.</string>
|
||||
<string name="disable_media_tunneling_title">Lülita meedia tunneldamine välja</string>
|
||||
<string name="drawer_header_description">Vaheta teenust, hetkel on kasutusel:</string>
|
||||
@ -609,9 +607,7 @@
|
||||
<string name="feed_use_dedicated_fetch_method_enable_button">Luba kiire režiim</string>
|
||||
<string name="feed_use_dedicated_fetch_method_title">Hangi võimalusel spetsiaalsest voost</string>
|
||||
<string name="feed_load_error_fast_unknown">Kiirvoo režiim ei paku selle kohta täiendavat teavet.</string>
|
||||
<string name="feed_load_error_terminated">Autori konto on lõpetatud.
|
||||
\nTulevikus ei saa NewPipe seda voogu laadida.
|
||||
\nKas soovid tühistada selle kanali tellimuse\?</string>
|
||||
<string name="feed_load_error_terminated">Autori konto on suletud. \nTulevikus ei saa NewPipe seda meediavoogu laadida. \nKas soovid tühistada selle kanali tellimuse?</string>
|
||||
<string name="feed_load_error_account_info">Voo \'%s\' laadimine nurjus.</string>
|
||||
<string name="feed_load_error">Via voo laadimisel</string>
|
||||
<string name="feed_update_threshold_option_always_update">Värskenda alati</string>
|
||||
@ -622,17 +618,7 @@
|
||||
<string name="downloads_storage_use_saf_summary_api_29">Android 10st alates on toetatud ainult salvestusjuurdepääsu raamistik \'Storage Access Framework\'</string>
|
||||
<string name="downloads_storage_ask_summary_no_saf_notice">Sinult küsitakse iga kord, kuhu alla laadimine salvestada</string>
|
||||
<string name="detail_heart_img_view_description">Südamlik autor</string>
|
||||
<string name="feed_use_dedicated_fetch_method_help_text">Kas sinu meelest on voo laadimine aeglane\? Sel juhul proovi lubada kiire laadimine (seda saad muuta seadetes või vajutades allolevat nuppu).
|
||||
\n
|
||||
\nNewPipe pakub kahte voo laadimise strateegiat:
|
||||
\n• Tellitud kanali täielik, kuid aeglane hankimine.
|
||||
\n• Teenuse spetsiaalse lõpp-punkti kasutamine, mis on kiire, kuid tavaliselt mittetäielik.
|
||||
\n
|
||||
\nErinevus nende kahe vahel seisneb selles, et kiirel puudub tavaliselt teave, näiteks üksuse pikkus või tüüp (ei saa eristada reaalajas videoid tavalistest) ja see võib tagastada vähem üksusi.
|
||||
\n
|
||||
\nYouTube on näide teenusest, mis pakub seda kiirmeetodit oma RSS-vooga.
|
||||
\n
|
||||
\nNii et valik taandub sellele, mida eelistad: kiirus või täpne teave.</string>
|
||||
<string name="feed_use_dedicated_fetch_method_help_text">Kas sinu meelest on voo laadimine aeglane? Sel juhul proovi lubada kiire laadimine (seda saad muuta seadetes või vajutades allolevat nuppu). \n \nNewPipe pakub kahte voo laadimise strateegiat: \n• Tellitud kanali täielik, kuid aeglane hankimine. \n• Teenuse spetsiaalse otspunkti kasutamine, mis on kiire, kuid tavaliselt mittetäielik. \n \nErinevus nende kahe vahel seisneb selles, et kiirel puudub tavaliselt teave, näiteks üksuse pikkus või tüüp (ei saa eristada reaalajas videoid tavalistest) ja see võib tagastada vähem üksusi. \n \nYouTube on näide teenusest, mis pakub seda kiirmeetodit oma RSS-vooga. \n \nNii et valik taandub sellele, mida eelistad: kiirus või täpne teave.</string>
|
||||
<string name="mark_as_watched">Märgi vaadatuks</string>
|
||||
<string name="show_image_indicators_summary">Näita piltide kohal Picasso värvides riba, mis märgib pildi allikat: punane tähistab võrku, sinine kohalikku andmekandjat ja roheline kohalikku mälu</string>
|
||||
<string name="show_image_indicators_title">Näita piltide allikat</string>
|
||||
@ -658,7 +644,7 @@
|
||||
<string name="feed_new_items">Uued andmevoo kirjed</string>
|
||||
<string name="show_crash_the_player_title">Näita „Jooksuta meediamängija kokku“ nupukest</string>
|
||||
<string name="show_crash_the_player_summary">Näitab valikut meediamängija kokkujooksutamiseks</string>
|
||||
<string name="error_report_notification_title">NewPipe töös tekkis viga, sellest teavitamiseks toksa</string>
|
||||
<string name="error_report_notification_title">NewPipe\'i töös tekkis viga, sellest teavitamiseks toksa</string>
|
||||
<string name="crash_the_player">Jooksuta meediamängija kokku</string>
|
||||
<string name="show_error_snackbar">Näita veateate akent</string>
|
||||
<string name="error_report_channel_name">Teavitus vigadest</string>
|
||||
@ -804,7 +790,6 @@
|
||||
<string name="share_playlist_content_details">%1$s
|
||||
\n%2$s</string>
|
||||
<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">
|
||||
@ -814,8 +799,7 @@
|
||||
<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="auto_update_check_description">NewPipe võib aeg-ajalt automaatselt kontrollida uute versioonide olemasolu ning sind vastavalt teavitada. \nKas sa soovid sellist võimalust kasutada?</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>
|
||||
@ -824,6 +808,6 @@
|
||||
\nKas sa soovid jätkata?</string>
|
||||
<string name="yes">Jah</string>
|
||||
<string name="no">Ei</string>
|
||||
<string name="import_settings_vulnerable_format">Imporditavad andmed kasutavad turvaprobleemidega vormingut, mida alates versioonist 0.27.0 NewPipe enam luua ei suuda. Palun kontrolli, et impordifail on loodud usaldusväärse osapoole poolt ning edaspidi loo ekspordifailid NewPipe versiooniga 0.27.0 või uuemaga. Tugi sellise vana vormingu kasutamisele kaob õige pea ja seejärel NewPipe uuemad ja vanemad versioonid ei saa omavahel andmeid enam vahetada.</string>
|
||||
<string name="import_settings_vulnerable_format">Imporditavad andmed kasutavad turvaprobleemidega vormingut, mida alates versioonist 0.27.0 NewPipe enam kasutada ei suuda. Palun kontrolli, et impordifail on loodud usaldusväärse osapoole poolt ning eelista ekspordifaile, mis on loodud NewPipe\'i versiooniga 0.27.0 või uuemaga. Tugi sellise vana vormingu kasutamisele kaob õige pea ja seejärel NewPipe\'i uuemad ja vanemad versioonid ei saa omavahel andmeid enam vahetada.</string>
|
||||
<string name="audio_track_type_secondary">täiendav</string>
|
||||
</resources>
|
@ -774,7 +774,6 @@
|
||||
<string name="notification_actions_summary_android13">Editatu beheko jakinarazpen ekintza bakoitza gainean sakatuz. Lehen hiru ekintzak (erreproduzitu/pausatu, aurrekoa eta hurrengoa) sistemarengatik ezarrita daude eta ezin dira pertsonalizatu.</string>
|
||||
<string name="rewind">Atzera egin</string>
|
||||
<string name="image_quality_title">Irudiaren kalitatea</string>
|
||||
<string name="share_playlist_with_titles_message">Partekatu erreprodukzio-zerrenda xehetasunekin, esate baterako, erreprodukzio-zerrendaren izena eta bideo-izenburuak edo bideo-URLen zerrenda soil gisa</string>
|
||||
<string name="more_options">Aukera gehiago</string>
|
||||
<string name="duration">Iraupena</string>
|
||||
<string name="forward">Aurrera egin</string>
|
||||
|
@ -764,4 +764,6 @@
|
||||
<string name="duration">مدّت</string>
|
||||
<string name="rewind">پسروی</string>
|
||||
<string name="question_mark">؟</string>
|
||||
<string name="settings_category_backup_restore_title">پشتیبانگیری و بازیابی</string>
|
||||
<string name="no_live_streams">بدون جریان زنده</string>
|
||||
</resources>
|
@ -759,7 +759,6 @@
|
||||
<string name="rewind">Kelaa taaksepäin</string>
|
||||
<string name="feed_fetch_channel_tabs_summary">Noudettavat välilehdet syötettä päivitettäessä. Tällä valinnalla ei ole vaikutusta, jos kanava päivitetään käyttämällä nopeaa tilaa.</string>
|
||||
<string name="delete_downloaded_files_confirm">Poistetaanko kaikki ladatut tiedostot levyltä\?</string>
|
||||
<string name="share_playlist_with_titles_message">Jaa soittolista, jossa on tietoja, kuten soittolistan nimi ja videon nimi, tai yksinkertainen luettelo videoiden URL-osoitteista</string>
|
||||
<string name="image_quality_medium">Keskilaatu</string>
|
||||
<string name="metadata_uploader_avatars">Lataajan avatarit</string>
|
||||
<string name="percent">Prosentti</string>
|
||||
|
@ -178,7 +178,7 @@
|
||||
<string name="play_queue_stream_detail">Détails</string>
|
||||
<string name="play_queue_audio_settings">Paramètres audios</string>
|
||||
<string name="show_hold_to_append_title">Afficher l\'astuce « Maintenir pour ajouter à la file »</string>
|
||||
<string name="show_hold_to_append_summary">Affiche l’astuce lors de l’appui des boutons « Arrière-plan » ou « Flottant » sur la page de détails d’une vidéo</string>
|
||||
<string name="show_hold_to_append_summary">Affiche l’astuce lors de l’appui des boutons « Arrière-plan » ou « Flottant » sur la page de détails d’une vidéo</string>
|
||||
<string name="unknown_content">[Inconnu]</string>
|
||||
<string name="player_recoverable_failure">Récupération depuis l’erreur du lecteur</string>
|
||||
<string name="kiosk_page_summary">Kiosque</string>
|
||||
@ -197,7 +197,7 @@
|
||||
<string name="switch_to_main">Basculer en principal</string>
|
||||
<string name="drawer_open">Ouvrir le menu</string>
|
||||
<string name="drawer_close">Fermer le menu</string>
|
||||
<string name="no_player_found_toast">Aucun lecteur multimédia trouvé (vous pouvez installer VLC pour continuer).</string>
|
||||
<string name="no_player_found_toast">Aucun lecteur de flux trouvé (vous pouvez installer VLC pour le lire).</string>
|
||||
<string name="always">Toujours</string>
|
||||
<string name="just_once">Une seule fois</string>
|
||||
<string name="external_player_unsupported_link_type">Les lecteurs externes ne prennent pas en charge ces types de liens</string>
|
||||
@ -534,7 +534,7 @@
|
||||
<string name="channel_created_by">Créé par %s</string>
|
||||
<string name="show_original_time_ago_summary">Les textes originaux des services vont être visibles dans les items</string>
|
||||
<string name="show_original_time_ago_title">Afficher la date originelle sur les items</string>
|
||||
<string name="youtube_restricted_mode_enabled_title">Activer le « Mode restreint » de YouTube</string>
|
||||
<string name="youtube_restricted_mode_enabled_title">Activer le « Mode restreint » de YouTube</string>
|
||||
<string name="feed_group_show_only_ungrouped_subscriptions">Afficher uniquement les abonnements non groupés</string>
|
||||
<string name="playlist_page_summary">Page des listes de lecture</string>
|
||||
<string name="no_playlist_bookmarked_yet">Aucune liste de lecture encore enregistrée</string>
|
||||
@ -671,7 +671,7 @@
|
||||
<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>
|
||||
<string name="show_crash_the_player_title">Afficher « Faire planter le lecteur »</string>
|
||||
<string name="show_crash_the_player_summary">Montrer une option de plantage lors de l\'utilisation du lecteur</string>
|
||||
<string name="error_report_channel_name">Notification de rapport d\'erreur</string>
|
||||
<string name="error_report_channel_description">Notifications pour signaler les erreurs</string>
|
||||
@ -817,7 +817,6 @@
|
||||
<string name="forward">Avancer</string>
|
||||
<string name="rewind">Rembobiner</string>
|
||||
<string name="replay">Rejouer</string>
|
||||
<string name="share_playlist_with_titles_message">Partager la liste de lecture avec des détails tel que son nom et le titre de ses vidéos ou simplement la liste des URLs des vidéos</string>
|
||||
<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>
|
||||
@ -827,7 +826,7 @@
|
||||
<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_more">Voir 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>
|
||||
@ -841,4 +840,5 @@
|
||||
<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>
|
||||
<string name="import_settings_vulnerable_format">Les paramètres de l\'export en cours d\'importation utilisent un format vulnérable qui a été déprécié depuis NewPipe 0.27.0. Assurez-vous que l\'export en cours d\'importation provient d\'une source fiable. Privilégiez les exports obtenues à partir de NewPipe 0.27.0 ou des versions plus récentes à l\'avenir. Le support pour l\'importation des paramètres dans ce format vulnérable sera bientôt complètement supprimé et les anciennes versions de NewPipe ne pourront plus importer les paramètres des exports des nouvelles versions.</string>
|
||||
<string name="audio_track_type_secondary">secondaire</string>
|
||||
</resources>
|
@ -815,7 +815,6 @@
|
||||
<string name="metadata_banners">Encabezados</string>
|
||||
<string name="show_channel_tabs_summary">Lapelas a mostrar nas páxinas das canles</string>
|
||||
<string name="image_quality_summary">Escolla da calidade das imaxes e se cargar as imaxes na súa totalidade, para reducir o uso de datos e memoria. Os cambios limpan a caché das imaxes na memoria e no disco - %s</string>
|
||||
<string name="share_playlist_with_titles_message">Compartir a lista de reprodución con detalles como o nome da lista e os títulos dos videos ou como unha lista sinxela cos enlaces URL dos videos</string>
|
||||
<string name="share_playlist_with_list">Compartir lista de URLs</string>
|
||||
<string name="import_settings_vulnerable_format">A configuración da exportación a ser importada emprega un formato vulnerable que fica obsoleto dende NewPipe 0.27.0. Comprobe que a exportación que está a importar proveña dunha fonte fiable e preferibelmente empregue exportacións de NewPipe 0.27.0 ou posterior. A compatibilidade coa importación deste formato vulnerable será eliminada por completo próximamente e as versión antigas de NewPipe non poderán importar configuracións de exportacións dende novas versións.</string>
|
||||
<string name="channel_tab_tracks">Pistas</string>
|
||||
|
@ -3,7 +3,7 @@
|
||||
<string name="black_theme_title">કાળું</string>
|
||||
<string name="dark_theme_title">અંધારિયું</string>
|
||||
<string name="light_theme_title">પ્રકાશ</string>
|
||||
<string name="night_theme_title">નાઇટ થીમ</string>
|
||||
<string name="night_theme_title">રાત્રિ થીમ</string>
|
||||
<string name="theme_title">થીમ</string>
|
||||
<string name="default_video_format_title">ડિફોલ્ટ વિડિઓ ફોર્મેટ</string>
|
||||
<string name="default_audio_format_title">ડિફોલ્ટ ઓડિયો ફોર્મેટ</string>
|
||||
@ -15,16 +15,16 @@
|
||||
<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>
|
||||
<string name="notification_action_1_title">બીજું ક્રિયા બટન</string>
|
||||
<string name="notification_action_0_title">પ્રથમ ક્રિયા બટન</string>
|
||||
<string name="notification_scale_to_square_image_summary">સૂચનામાં 16: 9 થી 1: 1 અસ્પેક્ટ રેશિયોમાં બતાવેલ વિડિઓ થંબનેલને સ્કેલ કરો (વિકૃતિ રજૂ કરી શકે છે)</string>
|
||||
<string name="notification_scale_to_square_image_title">સ્કેલ થંબનેલથી 1: 1 પાસા રેશિયો</string>
|
||||
<string name="show_play_with_kodi_summary">કોડી મીડિયા સેન્ટર દ્વારા વિડિઓ ચલાવવાનો વિકલ્પ દર્શાવો</string>
|
||||
<string name="kore_not_found">અનુપસ્થિત Kode એપ્લિકેશન ઇન્સ્ટોલ કરીએ\?</string>
|
||||
<string name="notification_scale_to_square_image_summary">સૂચનામાં બતાવેલ વિડિઓ થંબનેલને ૧૬:૯ થી ૧:૧ સાપેક્ષ ગુણોત્તરમાં કાપો</string>
|
||||
<string name="notification_scale_to_square_image_title">થંબનેલને ૧:૧ સાપેક્ષ ગુણોત્તરમાં કાપો</string>
|
||||
<string name="show_play_with_kodi_summary">કોડિ મીડિયા સેન્ટર દ્વારા વિડિઓ ચલાવવાનો વિકલ્પ દર્શાવો</string>
|
||||
<string name="kore_not_found">અનુપસ્થિત Kore અનુપ્રયોગ સ્થાપિત કરીએ?</string>
|
||||
<string name="show_higher_resolutions_summary">ફક્ત થોડા ઉપકરણો 2K / 4K વિડિઓઝ ચલાવી શકે છે</string>
|
||||
<string name="show_higher_resolutions_title">ઉચ્ચ રીઝોલ્યુશન બતાવો</string>
|
||||
<string name="default_popup_resolution_title">ડિફોલ્ટ પોપઅપ રીઝોલ્યુશન</string>
|
||||
@ -40,7 +40,7 @@
|
||||
<string name="controls_background_title">પૃષ્ઠભૂમિ</string>
|
||||
<string name="tab_choose">ટેબ પસંદ કરો</string>
|
||||
<string name="tab_bookmarks">બુકમાર્ક કરેલ પ્લેલિસ્ટ્સ</string>
|
||||
<string name="tab_subscriptions">સબ્સ્ક્રિપ્શન્સ</string>
|
||||
<string name="tab_subscriptions">લવાજમઓ</string>
|
||||
<string name="show_info">માહિતી બતાવો</string>
|
||||
<string name="subscription_update_failed">સબ્સ્ક્રિપ્શન અપડેટ કરી શકાયું નથી</string>
|
||||
<string name="subscription_change_failed">સબ્સ્ક્રિપ્શન બદલી શકાયું નહીં</string>
|
||||
@ -68,4 +68,19 @@
|
||||
<string name="no_player_found">કોઈ સ્ટ્રીમ પ્લેયર મળ્યો નથી. વીએલસી સ્થાપિત કરીએ\?</string>
|
||||
<string name="upload_date_text">%1$s પર પ્રકાશિત</string>
|
||||
<string name="main_bg_subtitle">પ્રારંભ કરવા માટે વિપુલ - દર્શક કાચને ટેપ કરો.</string>
|
||||
<string name="mark_as_watched">જોયેલું તરીકે ચિહ્નિત કરો</string>
|
||||
<string name="ok">ઠીક છે</string>
|
||||
<string name="yes">હા</string>
|
||||
<string name="no">ના</string>
|
||||
<string name="trending">વલણમાંનાં</string>
|
||||
<string name="auto_queue_toggle">આપોઆપ કતારબદ્ધતા</string>
|
||||
<string name="crash_the_player">પ્લેયરને ક્રેશ કરો</string>
|
||||
<string name="action_history">ઇતિહાસ</string>
|
||||
<string name="play_with_kodi_title">કોટિથી ચલાવો</string>
|
||||
<string name="show_play_with_kodi_title">કોડિથી ચલાવવાનો વિકલ્પ દેખાડો</string>
|
||||
<string name="download_dialog_title">ડાઉનલોડ કરો</string>
|
||||
<string name="autoplay_title">આપમેળે ચલાવો</string>
|
||||
<string name="fragment_feed_title">નવું શું છે</string>
|
||||
<string name="downloads">ડાઉનલોડ્સ</string>
|
||||
<string name="downloads_title">ડાઉનલોડ્સ</string>
|
||||
</resources>
|
@ -830,7 +830,6 @@
|
||||
<string name="share_playlist_content_details">%1$s
|
||||
\n%2$s</string>
|
||||
<string name="share_playlist">שיתוף רשימת נגינה</string>
|
||||
<string name="share_playlist_with_titles_message">שיתוף רשימת נגינה עם פרטים כגון שם רשימת נגינה וכותרות סרטונים או כרשימה פשוטה של כתובות סרטונים</string>
|
||||
<string name="video_details_list_item">- %1$s: %2$s</string>
|
||||
<string name="show_more">להציג עוד</string>
|
||||
<string name="show_less">להציג פחות</string>
|
||||
@ -852,4 +851,5 @@
|
||||
\nלהמשיך?</string>
|
||||
<string name="error_insufficient_storage">אין מספיק מקום פנוי במכשיר</string>
|
||||
<string name="import_settings_vulnerable_format">ההגדרות בייצוא המיובא משתמשות בתסדיר פגיע שהוצא משימוש מאז NewPipe 0.27.0. יש לוודא שהייצוא המיובא הוא ממקור מהימן, ועדיף להשתמש רק בייצוא שהושג מ־NewPipe 0.27.0 ומעלה בעתיד. תמיכה בייבוא הגדרות בתסדיר פגיע זה תוסר בקרוב לחלוטין, ואז גרסאות ישנות של NewPipe לא יוכלו לייבא עוד הגדרות של ייצוא מגרסאות חדשות.</string>
|
||||
<string name="audio_track_type_secondary">משני</string>
|
||||
</resources>
|
@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="upload_date_text">%1$s पे प्रकाशित हुआ</string>
|
||||
<string name="no_player_found">स्ट्रीमिंग के लिए प्लेयर नहीं मिला। क्या आप वीएलसी इंस्टॉल करना चाहेंगे\?</string>
|
||||
<string name="no_player_found">स्ट्रीमिंग के लिए प्लेयर नहीं मिला। क्या आप VLC इंस्टॉल करना चाहेंगे?</string>
|
||||
<string name="install">इंस्टॉल करें</string>
|
||||
<string name="open_in_browser">ब्राउज़र में खोलें</string>
|
||||
<string name="open_in_popup_mode">पॉपअप मोड में खोलें</string>
|
||||
@ -183,7 +183,7 @@
|
||||
<string name="hold_to_append">कतार में जोड़ने के लिए दबाकर रखें</string>
|
||||
<string name="start_here_on_background">बैकग्राउंड में चलाना शुरू करें</string>
|
||||
<string name="start_here_on_popup">पॉपअप में चलाना शुरू करें</string>
|
||||
<string name="no_player_found_toast">स्ट्रीमिंग करने के लिए प्लेयर नहीं मिला (आप इसे चलाने के लिए वीएलसी प्लेयर इंस्टॉल कर सकते हैं)।</string>
|
||||
<string name="no_player_found_toast">स्ट्रीमिंग करने के लिए प्लेयर नहीं मिला (आप इसे चलाने के लिए VLC प्लेयर इंस्टॉल कर सकते हैं)।</string>
|
||||
<string name="controls_download_desc">स्ट्रीम फाइल डाउनलोड करें</string>
|
||||
<string name="show_info">जानकारी दिखाएं</string>
|
||||
<string name="tab_bookmarks">बुकमार्क की गई प्लेलिस्टें</string>
|
||||
@ -804,7 +804,6 @@
|
||||
<string name="share_playlist_content_details">%1$s
|
||||
\n%2$s</string>
|
||||
<string name="share_playlist">प्लेलिस्ट साझा करें</string>
|
||||
<string name="share_playlist_with_titles_message">प्लेलिस्ट को प्लेलिस्ट नाम और वीडियो शीर्षक जैसे विवरण के साथ या वीडियो यूआरएल की एक सरल सूची के रूप में साझा करें</string>
|
||||
<string name="video_details_list_item">- %1$s: %2$s</string>
|
||||
<plurals name="replies">
|
||||
<item quantity="one">%s जवाब</item>
|
||||
|
@ -424,8 +424,8 @@
|
||||
<string name="feed_group_dialog_delete_message">Želiš li izbrisati ovu grupu\?</string>
|
||||
<string name="feed_create_new_group_button_title">Nova</string>
|
||||
<string name="feed_update_threshold_option_always_update">Uvijek aktualiziraj</string>
|
||||
<string name="feed_use_dedicated_fetch_method_enable_button">Uključi brzi način</string>
|
||||
<string name="feed_use_dedicated_fetch_method_disable_button">Isključi brzi način</string>
|
||||
<string name="feed_use_dedicated_fetch_method_enable_button">Uključi brzi modus</string>
|
||||
<string name="feed_use_dedicated_fetch_method_disable_button">Isključi brzi modus</string>
|
||||
<string name="error_insufficient_storage_left">Memorija uređaja je popunjena</string>
|
||||
<string name="most_liked">Najomiljeniji</string>
|
||||
<string name="subtitle_activity_recaptcha">Pritisni „Gotovo” kad je riješeno</string>
|
||||
@ -639,7 +639,7 @@
|
||||
<string name="no_dir_yet">Mapa za preuzimanje još nije postavljena, odaberi standardnu mapu za preuzimanje</string>
|
||||
<string name="comments_are_disabled">Komentari su isključeni</string>
|
||||
<string name="mark_as_watched">Označi kao pogledano</string>
|
||||
<string name="feed_load_error_fast_unknown">Način rada brzog feeda ne pruža više informacija o ovome.</string>
|
||||
<string name="feed_load_error_fast_unknown">Brzi modus feeda ne pruža više informacija o ovome.</string>
|
||||
<string name="metadata_privacy_internal">Interno</string>
|
||||
<string name="metadata_privacy">Privatnost</string>
|
||||
<string name="description_select_note">Sada možeš odabrati tekst u opisu. Napomena: stranica će možda treperiti i možda nećeš moći kliknuti poveznice u načinu rada za odabir teksta.</string>
|
||||
@ -800,7 +800,6 @@
|
||||
<string name="image_quality_medium">Srednja kvaliteta</string>
|
||||
<string name="image_quality_high">Visoka kvaliteta</string>
|
||||
<string name="question_mark">\?</string>
|
||||
<string name="share_playlist_with_titles_message">Dijeli playlistu s detaljima kao što su ime playliste i naslovi videa ili kao jednostavan popis URL-ova videa</string>
|
||||
<string name="share_playlist_with_titles">Dijeli s naslovima</string>
|
||||
<string name="share_playlist_with_list">Dijeli popis URL-ova</string>
|
||||
<string name="video_details_list_item">– %1$s: %2$s</string>
|
||||
@ -833,4 +832,7 @@
|
||||
<string name="reset_all_settings">Obnavljanje svih postavki odbacit će sve tvoje postavljene postavke i aplikacija će se ponovo pokrenuti.
|
||||
\n
|
||||
\nStvarno želiš nastaviti?</string>
|
||||
<string name="always_use_exoplayer_set_output_surface_workaround_title">Uvijek koristi ExoPlayer postavku zaobilaženja videa za izlaznu površinu</string>
|
||||
<string name="feed_fetch_channel_tabs_summary">Kartice za dohvaćanje prilikom aktualiziranja feeda. Ova opcija nema učinka ako se kanal aktualizira pomoću brzog modusa.</string>
|
||||
<string name="audio_track_type_secondary">sekundarno</string>
|
||||
</resources>
|
@ -324,7 +324,7 @@
|
||||
<string name="app_update_notification_channel_name">Alkalmazásfrissítés értesítése</string>
|
||||
<string name="file_deleted">Fájl törölve</string>
|
||||
<string name="settings_category_updates_title">Frissítések</string>
|
||||
<string name="show_hold_to_append_summary">Tipp megjelenítése, ha megnyomja a hátteret vagy a felugró gombot a videó részleteinél</string>
|
||||
<string name="show_hold_to_append_summary">Tipp megjelenítése a háttér vagy a felugró gomb megnyomásakor a videó „Részletek:” lehetőségnél</string>
|
||||
<string name="autoplay_title">Automatikus lejátszás</string>
|
||||
<string name="settings_category_clear_data_title">Adatok törlése</string>
|
||||
<string name="enable_playback_state_lists_summary">Lejátszási pozíciók megjelenítése a listákban</string>
|
||||
@ -534,7 +534,7 @@
|
||||
<string name="hash_channel_description">Értesítések a videók ujjlenyomatkészítési folyamatához</string>
|
||||
<string name="hash_channel_name">Videó ujjlenyomat-készítési értesítése</string>
|
||||
<string name="youtube_restricted_mode_enabled_summary">A YouTube biztosít egy „Korlátozott módot”, amely elrejti a lehetséges felnőtteknek szóló tartalmat</string>
|
||||
<string name="youtube_restricted_mode_enabled_title">A YouTube „Korlátozott módjának” bekapcsolása</string>
|
||||
<string name="youtube_restricted_mode_enabled_title">A YouTube „Korlátozott mód” bekapcsolása</string>
|
||||
<string name="peertube_instance_add_exists">A példány már létezik</string>
|
||||
<string name="peertube_instance_add_fail">A példány érvényesítése nem sikerült</string>
|
||||
<string name="peertube_instance_add_help">Adja meg a példány webcímét</string>
|
||||
@ -658,7 +658,7 @@
|
||||
<string name="show_original_time_ago_summary">A szolgáltatásokból származó eredeti szövegek láthatók lesznek a közvetítési elemeken</string>
|
||||
<string name="crash_the_player">Lejátszó összeomlasztása</string>
|
||||
<string name="show_image_indicators_title">Képjelölők megjelenítése</string>
|
||||
<string name="show_crash_the_player_title">A „lejátszó összeomlasztása” lehetőség megjelenítése</string>
|
||||
<string name="show_crash_the_player_title">A „Lejátszó összeomlasztása” lehetőség megjelenítése</string>
|
||||
<string name="show_crash_the_player_summary">Megjeleníti az összeomlasztási lehetőséget a lejátszó használatakor</string>
|
||||
<string name="unhook_checkbox">Hangmagasság megtartása (torzítást okozhat)</string>
|
||||
<string name="check_for_updates">Frissítések keresése</string>
|
||||
@ -796,7 +796,6 @@
|
||||
<string name="image_quality_high">Magas minőségű</string>
|
||||
<string name="question_mark">\?</string>
|
||||
<string name="share_playlist">Lejátszási lista megosztása</string>
|
||||
<string name="share_playlist_with_titles_message">Lejátszási lista megosztása olyan részletekkel, mint például a lejátszási lista neve és a videó címe, vagy a videó webcímek egyszerű listájaként</string>
|
||||
<string name="share_playlist_with_titles">Megosztás címekkel</string>
|
||||
<string name="share_playlist_content_details">%1$s
|
||||
\n%2$s</string>
|
||||
|
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="main_bg_subtitle">Սեղմեք որոնման կոճակը որ սկսել</string>
|
||||
<string name="main_bg_subtitle">Սեղմեք խոշորացույցը որ սկսեք</string>
|
||||
<string name="search">Որոնել</string>
|
||||
<string name="downloads">Բեռնված</string>
|
||||
<string name="downloads_title">Բեռնված</string>
|
||||
@ -228,7 +228,7 @@
|
||||
<string name="sort">Դասավորել</string>
|
||||
<string name="detail_pinned_comment_view_description">Գամված մեկնաբանություն</string>
|
||||
<string name="account_terminated">Հաշիվը կասեցված է</string>
|
||||
<string name="channel_tab_about"></string>
|
||||
<string name="channel_tab_about"/>
|
||||
<string name="channel_tab_albums">Ալբոմներ</string>
|
||||
<string name="yes">Այո</string>
|
||||
<string name="no">Ոչ</string>
|
||||
@ -241,4 +241,6 @@
|
||||
<string name="channel_tab_channels">Ալիքներ</string>
|
||||
<string name="channel_tab_livestreams">Ուղիղ</string>
|
||||
<string name="unknown_audio_track">Անհայտ</string>
|
||||
<string name="did_you_mean">Նկատի ունե՞ս «%1$s»</string>
|
||||
<string name="volume">Բարձրություն</string>
|
||||
</resources>
|
@ -138,7 +138,7 @@
|
||||
<string name="auto_queue_summary">Melanjutkan akhir dari antrean pemutaran (tak berulang) dengan menambahkan video terkait</string>
|
||||
<string name="enable_watch_history_summary">Simpan daftar video yang telah ditonton</string>
|
||||
<string name="show_hold_to_append_title">Tip \"Tahan untuk menambahkan\"</string>
|
||||
<string name="show_hold_to_append_summary">Tampilkan tip ketika menekan tombol latar belakang atau popup di dalam \"Detail:\" video</string>
|
||||
<string name="show_hold_to_append_summary">Tampilkan tip ketika menekan tombol latar belakang atau popup di dalam video \"Detail:\"</string>
|
||||
<string name="default_content_country_title">Lokasi Konten</string>
|
||||
<string name="settings_category_player_title">Pemutar</string>
|
||||
<string name="settings_category_player_behavior_title">Perilaku</string>
|
||||
@ -508,7 +508,7 @@
|
||||
\nJadi pilihlah yang sesuai yang Anda inginkan: kecepatan atau kelengkapan informasi.</string>
|
||||
<string name="show_original_time_ago_summary">Teks asli dari layanan akan ditampilkan di dalam video</string>
|
||||
<string name="show_original_time_ago_title">Tampilkan waktu yang lalu sebenarnya pada item</string>
|
||||
<string name="youtube_restricted_mode_enabled_title">Aktifkan \"Mode Terbatas\" YouTube</string>
|
||||
<string name="youtube_restricted_mode_enabled_title">Aktifkan \"Mode Terbatas\"</string>
|
||||
<string name="video_detail_by">Oleh %s</string>
|
||||
<string name="channel_created_by">Dibuat oleh %s</string>
|
||||
<string name="detail_sub_channel_thumbnail_view_description">Thumbnail avatar channel</string>
|
||||
@ -790,7 +790,6 @@
|
||||
<string name="share_playlist_content_details">%1$s
|
||||
\n%2$s</string>
|
||||
<string name="share_playlist">Bagikan Daftar Putar</string>
|
||||
<string name="share_playlist_with_titles_message">Bagikan daftar putar dengan detail seperti nama daftar putar dan judul video atau sebagai daftar video URL yang sederhana</string>
|
||||
<string name="metadata_banners">Panji</string>
|
||||
<string name="video_details_list_item">- %1$s: %2$s</string>
|
||||
<string name="notification_actions_summary_android13">Sentuh untuk menyunting tindakan notifikasi di bawah. Tiga tindakan pertama (mainkan/jeda, sebelumnya dan selanjutnya) disetel oleh sistem dan tidak bisa dikustomisasi.</string>
|
||||
|
@ -607,7 +607,7 @@
|
||||
<string name="enable_playback_state_lists_title">Spilunarstöður í listum</string>
|
||||
<string name="enable_playback_state_lists_summary">Sýna spilunarstöður í listum</string>
|
||||
<string name="show_hold_to_append_title">Sýna ábendinguna „Haltu niðri til að bæta við spilunarröð“</string>
|
||||
<string name="show_hold_to_append_summary">Sýna ábendingu þegar ýtt er á bakgrunninn eða sprettihnappinn í „Nánar:“ á myndskeiðinu</string>
|
||||
<string name="show_hold_to_append_summary">Sýna ábendingu þegar ýtt er á bakgrunninn eða sprettihnappinn á myndskeiðinu í „Nánar:\"</string>
|
||||
<string name="unsupported_url_dialog_message">Óþekkt slóð. Opna með öðru forriti\?</string>
|
||||
<string name="peertube_instance_url_summary">Veldu uppáhalds PeerTube tilvik þín</string>
|
||||
<string name="peertube_instance_url_help">Þú mátt finna tilviki á %s</string>
|
||||
@ -647,7 +647,7 @@
|
||||
<string name="disable_media_tunneling_summary">Slökktu á margmiðlunargöngum (media tunneling) ef vart verður við svartan skjá eða hökt við spilun myndskeiða.</string>
|
||||
<string name="show_image_indicators_title">Sýna myndvísa</string>
|
||||
<string name="show_image_indicators_summary">Sýna Picasso litaða borða ofan á myndum sem gefa til kynna uppruna þeirra: rauðan fyrir netið, bláan fyrir disk og grænan fyrir minni</string>
|
||||
<string name="show_crash_the_player_title">Sýna „Hrynja spilara“</string>
|
||||
<string name="show_crash_the_player_title">Sýna „Láta spilara hrynja\"</string>
|
||||
<string name="show_crash_the_player_summary">Sýna valkost til að hrynja spilara</string>
|
||||
<string name="crash_the_app">Hrynja forrit</string>
|
||||
<string name="create_error_notification">Búа til villutilkynningu</string>
|
||||
@ -730,7 +730,7 @@
|
||||
<string name="feed_show_upcoming">Á næstunni</string>
|
||||
<string name="channel_tab_videos">Myndskeið</string>
|
||||
<string name="channel_tab_tracks">Spor</string>
|
||||
<string name="channel_tab_shorts">Stuttmyndir</string>
|
||||
<string name="channel_tab_shorts">Símamyndir</string>
|
||||
<string name="channel_tab_playlists">Spilunarlistar</string>
|
||||
<string name="channel_tab_albums">Albúm</string>
|
||||
<string name="channel_tab_about">Um hugbúnaðinn</string>
|
||||
@ -792,7 +792,6 @@
|
||||
<string name="unset_playlist_thumbnail">Losa varanlega smámynd</string>
|
||||
<string name="notification_actions_summary_android13">Breyttu hverri tilkynningu hér fyrir neðan með því að ýta á hana. Fyrstu þrjár aðgerðirnar (spila/bíða, fyrra og næsta) eru skilgreindar af kerfinu og er því ekki hægt að sérsníða.</string>
|
||||
<string name="feed_fetch_channel_tabs_summary">Flipar sem á að sækja við uppfærslu þessa streymis. Þetta hefur engin áhrif ef rás er uppfærð með hraðstreymisham.</string>
|
||||
<string name="share_playlist_with_titles_message">Deildu spilunarlista með atriðum eins og heiti spilunarlistans og titlum myndskeiða eða sem einföldum lista yfir slóðir á myndskeið</string>
|
||||
<string name="use_exoplayer_decoder_fallback_title">Nota varaeiginleika ExoPlayer-afkóðarans</string>
|
||||
<string name="new_seek_duration_toast">Vegna takmarkana í ExoPlayer-spilaranum var tímalengd hoppa sett á %d sekúndur</string>
|
||||
<string name="disable_media_tunneling_automatic_info">Margmiðlunargöng (media tunneling) voru gerð óvirk á tækinu þínu þar sem þessi gerð tækja er þekkt fyrir að styðja ekki þennan eiginleika.</string>
|
||||
|
@ -96,8 +96,8 @@
|
||||
<string name="show_higher_resolutions_title">Mostra risoluzioni più elevate</string>
|
||||
<string name="show_higher_resolutions_summary">Solo alcuni dispositivi possono riprodurre video 2K/4K</string>
|
||||
<string name="default_video_format_title">Formato video predefinito</string>
|
||||
<string name="popup_remember_size_pos_title">Ricorda proprietà lettore popup</string>
|
||||
<string name="popup_remember_size_pos_summary">Ricorda dimensione e posizione del lettore popup</string>
|
||||
<string name="popup_remember_size_pos_title">Ricorda proprietà del popup</string>
|
||||
<string name="popup_remember_size_pos_summary">Ricorda ultima dimensione e posizione del popup</string>
|
||||
<string name="show_search_suggestions_title">Suggerimenti di ricerca</string>
|
||||
<string name="show_search_suggestions_summary">Scegli suggerimenti di ricerca</string>
|
||||
<string name="clear">Cancella</string>
|
||||
@ -139,7 +139,7 @@
|
||||
<string name="settings_category_player_title">Lettore multimediale</string>
|
||||
<string name="settings_category_player_behavior_title">Comportamento</string>
|
||||
<string name="settings_category_history_title">Cronologia e cache</string>
|
||||
<string name="undo">Annulla</string>
|
||||
<string name="undo">Rifai</string>
|
||||
<string name="notification_channel_name">Notifica NewPipe</string>
|
||||
<string name="notification_channel_description">Notifiche per il lettore multimediale di NewPipe</string>
|
||||
<string name="search_no_results">Nessun risultato</string>
|
||||
@ -817,7 +817,6 @@
|
||||
<string name="share_playlist_content_details">%1$s
|
||||
\n%2$s</string>
|
||||
<string name="share_playlist">Condividi playlist</string>
|
||||
<string name="share_playlist_with_titles_message">Condividi la playlist con dettagli come il suo nome e i titoli video o come un semplice elenco di URL video</string>
|
||||
<string name="video_details_list_item">- %1$s: %2$s</string>
|
||||
<plurals name="replies">
|
||||
<item quantity="one">%s risposta</item>
|
||||
@ -839,4 +838,5 @@
|
||||
<string name="auto_update_check_description">NewPipe può cercare automaticamente nuove versioni di tanto in tanto e avvisarti quando sono disponibili.
|
||||
\nVuoi attivarlo?</string>
|
||||
<string name="import_settings_vulnerable_format">Le impostazioni nell\'export che viene importato usano un formato vulnerabile che è stato deprecato dalla versione 0.27.0 di NewPipe. Assicuratevi che l\'export importato venga da una fonte fidata, sarebbe preferibile usare solo exports ottenuti da NewPipe 0.27.0 o superiori, nel futuro. Il supporto all\'importazione di Impostazioni in questo formato vulnerabile sarà presto rimosso completamente, da quel momento le versioni di NewPipe più vecchie non saranno più in grado di importare impostazioni tramite export di versioni più recenti.</string>
|
||||
<string name="audio_track_type_secondary">secondaria</string>
|
||||
</resources>
|
@ -791,7 +791,6 @@
|
||||
<string name="share_playlist_content_details">%1$s
|
||||
\n%2$s</string>
|
||||
<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="other">%sの返信</item>
|
||||
|
@ -145,4 +145,83 @@
|
||||
<string name="metadata_category">Taggayt</string>
|
||||
<string name="metadata_privacy">Tabaḍnit</string>
|
||||
<string name="metadata_licence">Turagt</string>
|
||||
<string name="no_videos">Ula d yiwet n tvidyutt</string>
|
||||
<string name="volume">Ableɣ</string>
|
||||
<string name="peertube_instance_add_title">Rnu tummant</string>
|
||||
<string name="albums">Ilbumen</string>
|
||||
<string name="recent">Melmi kan</string>
|
||||
<string name="drawer_close">Mdel umuɣ</string>
|
||||
<string name="open_with">Ldi s</string>
|
||||
<string name="show_description_title">Sken-d aglam</string>
|
||||
<string name="content_language_title">Tutlayt n ugbur amezwaru</string>
|
||||
<string name="network_error">Tuccḍa deg uẓeṭṭa</string>
|
||||
<string name="infinite_videos">∞ tividyutin</string>
|
||||
<string name="msg_wait">Ttxil arǧu…</string>
|
||||
<string name="title_activity_about">Ɣef NewPipe</string>
|
||||
<string name="video_player">Imeɣri n uvidyu</string>
|
||||
<string name="channel_tab_shorts">Shorts</string>
|
||||
<string name="channel_tab_livestreams">Srid</string>
|
||||
<string name="channel_tab_channels">Ibuda</string>
|
||||
<string name="duration">Tanzagt</string>
|
||||
<string name="question_mark">\?</string>
|
||||
<string name="image_quality_medium">Taɣara talemmast</string>
|
||||
<string name="image_quality_high">Taɣara tafellayt</string>
|
||||
<string name="show_more">Sken-d ugar</string>
|
||||
<string name="show_less">Sken-d drus</string>
|
||||
<string name="songs">Tizlatin</string>
|
||||
<string name="channel_tab_videos">Tividyutin</string>
|
||||
<string name="channel_tab_playlists">Tibdarin n tɣuri</string>
|
||||
<string name="channel_tab_albums">Ilbumen</string>
|
||||
<string name="channel_tab_about">Ɣef</string>
|
||||
<string name="rewind">Tuɣalin ɣer deffir</string>
|
||||
<string name="image_quality_title">Taɣara n tugna</string>
|
||||
<string name="sort">Smizzwer</string>
|
||||
<string name="audio_track_name">%1$s %2$s</string>
|
||||
<string name="import_file_title">Kter afaylu</string>
|
||||
<string name="feed_show_upcoming">Itteddu-d</string>
|
||||
<string name="drawer_open">Ldi umuɣ</string>
|
||||
<string name="none">Ula d yiwen</string>
|
||||
<string name="share_playlist_content_details">%1$s\n%2$s</string>
|
||||
<string name="yes">Ih</string>
|
||||
<string name="no">Uhu</string>
|
||||
<string name="play_with_kodi_title">Ɣeṛ-it-id s Kodi</string>
|
||||
<string name="brightness">Tafat</string>
|
||||
<string name="default_content_country_title">Tamurt n ugbur amezwaru</string>
|
||||
<string name="artists">Inaẓuren</string>
|
||||
<string name="notifications">Ilɣa</string>
|
||||
<string name="play_all">Ɣeṛ-itent-id akk</string>
|
||||
<string name="notification_channel_name">Ilɣa n NewPipe</string>
|
||||
<string name="comments_tab_description">Iwenniten</string>
|
||||
<string name="description_tab_description">Asnummel</string>
|
||||
<string name="recaptcha_solve">Fru</string>
|
||||
<string name="card">Takarḍa</string>
|
||||
<string name="chapters">Ixfawen</string>
|
||||
<string name="app_language_title">Tutlayt n wesnas</string>
|
||||
<string name="metadata_host">Asenneftaɣ</string>
|
||||
<string name="metadata_privacy_public">Azayez</string>
|
||||
<string name="metadata_privacy_internal">Tadigant</string>
|
||||
<string name="on">Irmed</string>
|
||||
<string name="off">Yensa</string>
|
||||
<string name="enumeration_comma">,</string>
|
||||
<string name="open_website_license">Ldi asmel Web</string>
|
||||
<string name="unknown_audio_track">Arussin</string>
|
||||
<string name="audio_track_type_original">aneṣli</string>
|
||||
<string name="unbookmark_playlist">Kkes ticreḍt</string>
|
||||
<string name="import_from">Kter seg</string>
|
||||
<string name="app_license_title">Turagt n NewPipe</string>
|
||||
<string name="read_full_license">Ɣeṛ turagt</string>
|
||||
<string name="error_http_not_found">Ur yettwaf ara</string>
|
||||
<string name="more_options">Ugar n tnefrunin</string>
|
||||
<string name="share_dialog_title">Bḍu d</string>
|
||||
<string name="error_details_headline">Talqayt :</string>
|
||||
<string name="title_most_played">Tid yettwaɣran s waṭas</string>
|
||||
<string name="tab_choose">Fren iccer</string>
|
||||
<string name="metadata_language">Tutlayt</string>
|
||||
<string name="unknown_content">[Arussin]</string>
|
||||
<string name="play">Ɣeṛ</string>
|
||||
<string name="what_device_headline">Talɣut:</string>
|
||||
<string name="metadata_support">Tadhalt</string>
|
||||
<string name="radio">Amaṭṭaf</string>
|
||||
<string name="metadata_tags">Tibzimin</string>
|
||||
<string name="blank_page_summary">Asebter d ilem</string>
|
||||
</resources>
|
@ -776,7 +776,6 @@
|
||||
<string name="rewind">되감기</string>
|
||||
<string name="replay">다시 재생</string>
|
||||
<string name="feed_fetch_channel_tabs_summary">피드를 업데이트할 때 가져올 탭입니다. 빠른 모드를 사용하여 채널을 업데이트하는 경우 이 옵션은 효과가 없습니다.</string>
|
||||
<string name="share_playlist_with_titles_message">재생목록 이름, 동영상 제목 등의 세부정보 또는 간단한 동영상 URL 목록으로 재생목록을 공유하세요</string>
|
||||
<string name="image_quality_medium">중간 품질</string>
|
||||
<string name="metadata_uploader_avatars">업로더 아바타</string>
|
||||
<string name="metadata_banners">배너</string>
|
||||
|
@ -42,7 +42,7 @@
|
||||
<string name="did_you_mean">Intendi \"%1$s\"\?</string>
|
||||
<string name="settings">Optiones</string>
|
||||
<string name="main_bg_subtitle">Tange \"Quaere\" ad initium</string>
|
||||
<string name="search">Quaere</string>
|
||||
<string name="search">Quaerere</string>
|
||||
<string name="download">Scarica</string>
|
||||
<string name="share">Condividi</string>
|
||||
<string name="open_with">Aperi apud</string>
|
||||
|
@ -825,7 +825,6 @@
|
||||
<string name="channel_tab_videos">Vaizdo įrašai</string>
|
||||
<string name="channel_tab_tracks">Takeliai</string>
|
||||
<string name="image_quality_summary">Pasirinkite paveikslėlių kokybę ir ar apskritai įkelti paveikslėlius, kad sumažintumėte duomenų ir atminties naudojimą. Pakeitimai išvalo atmintyje ir diske esančių vaizdų talpyklą - %s</string>
|
||||
<string name="share_playlist_with_titles_message">Dalintis grojaraščiu su tokia informacija kaip grojaraščio pavadinimas ir vaizdo įrašo pavadinimas arba paprastas vaizdo įrašų nuorodų sąrašas</string>
|
||||
<string name="share_playlist_with_titles">Dalintis su pavadinimais</string>
|
||||
<string name="share_playlist">Dalintis grojaraščiu</string>
|
||||
<string name="share_playlist_with_list">Dalintis nuorodų sąrašu</string>
|
||||
|
@ -824,7 +824,6 @@
|
||||
<string name="audio_track_name">%1$s %2$s</string>
|
||||
<string name="channel_tab_tracks">Skaņdarbi</string>
|
||||
<string name="channel_tab_shorts">Īsie video</string>
|
||||
<string name="share_playlist_with_titles_message">Kopīgot atskaņošanas saraksta nosaukumu un to video nosaukumus vai tikai atskaņošanas sarakstā iekļauto video URL saites</string>
|
||||
<string name="share_playlist">Kopīgot atskaņošanas sarakstu</string>
|
||||
<string name="share_playlist_with_titles">Kopīgot nosaukumus</string>
|
||||
<string name="import_settings_vulnerable_format">Importētā eksporta iestatījumi izmanto ievainojamo formātu, kas tika pārtraukts kopš NewPipe 0.27.0 versijas. Pārliecinieties, ka importētie dati ir no uzticama avota, un turpmāk ir vēlams izmantot tikai datus, kas veikti NewPipe 0.27.0 vai jaunākās versijās. Iestatījumu importēšanas atbalsts šajā neaizsargātajā formātā drīzumā tiks pilnībā aizvākts, un tad vecās NewPipe versijas vairs nevarēs importēt iestatījumus, kas veikti jaunajās versijās.</string>
|
||||
|
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="main_bg_subtitle">Притиснете „Барај“ за да започнете.</string>
|
||||
<string name="main_bg_subtitle">Притиснете на лупата за да започнете.</string>
|
||||
<string name="upload_date_text">Објавено на %1$s</string>
|
||||
<string name="no_player_found">Не е пронајден плеер за стримови. Сакате да инсталирате VLC\?</string>
|
||||
<string name="no_player_found_toast">Не е пронајден плеер за стримови (можеш да инсталираш VLC)</string>
|
||||
@ -13,7 +13,7 @@
|
||||
<string name="controls_download_desc">Превземи датотека за стрим</string>
|
||||
<string name="search">Пребарај</string>
|
||||
<string name="settings">Поставки</string>
|
||||
<string name="did_you_mean">Мислеше на: %1$s\?</string>
|
||||
<string name="did_you_mean">Можеби мислевте на „%1$s“?</string>
|
||||
<string name="share_dialog_title">Сподели со</string>
|
||||
<string name="use_external_video_player_title">Користи надворешен видео плеер</string>
|
||||
<string name="use_external_video_player_summary">Се губи звукот на НЕКОИ резолуции</string>
|
||||
@ -30,7 +30,7 @@
|
||||
<string name="controls_background_title">Позадина</string>
|
||||
<string name="controls_popup_title">Подпрозорче</string>
|
||||
<string name="controls_add_to_playlist_title">Додај на</string>
|
||||
<string name="download_path_title">Локација за зачувување на видеата</string>
|
||||
<string name="download_path_title">Папка за зачувување на видеата</string>
|
||||
<string name="download_path_summary">Преземените видеа се сместени овде</string>
|
||||
<string name="download_path_dialog_title">Одберете каде да ги зачувате видеата</string>
|
||||
<string name="download_path_audio_title">Фолдер за преземање на аудио</string>
|
||||
@ -54,32 +54,32 @@
|
||||
<string name="popup_remember_size_pos_title">Запамти го местото и големината на малиот прозорец</string>
|
||||
<string name="popup_remember_size_pos_summary">Запамти ја последната големина и место на прозорчето</string>
|
||||
<string name="use_inexact_seek_title">Брзо, непрецизно премотување</string>
|
||||
<string name="use_inexact_seek_summary">Со непрецизното премотување се пребарува побрзо, но со намалена презицност.</string>
|
||||
<string name="use_inexact_seek_summary">Со непрецизното премотување се премотува побрзо, но со намалена презицност. Премотувањето со по 5, 15 или 25 секунди нема да работи со оваа опција.</string>
|
||||
<string name="thumbnail_cache_wipe_complete_notice">Кешираните слики се избришани</string>
|
||||
<string name="metadata_cache_wipe_title">Избришете ги кешираните мета-податоци</string>
|
||||
<string name="metadata_cache_wipe_summary">Избришете ги сите кеш-податоци од веб-страни</string>
|
||||
<string name="metadata_cache_wipe_complete_notice">Мета-податоците се избришани</string>
|
||||
<string name="auto_queue_title">Автоматски постави го на листа на следно видео</string>
|
||||
<string name="auto_queue_summary">Автоматски прикачи поврзано видео кога ќе почне последното пуштено видео во неповторлива листа</string>
|
||||
<string name="auto_queue_title">Автоматски додај го во редослед следниот стрим</string>
|
||||
<string name="auto_queue_summary">Продолжи го завршеното (неповторливо) емитување додавајќи поврзан стрим</string>
|
||||
<string name="show_search_suggestions_title">Пребарајте предлози</string>
|
||||
<string name="show_search_suggestions_summary">Прикажи предлози при пребарување</string>
|
||||
<string name="show_search_suggestions_summary">Предлозите ќе се прикажуваат додека пребарувате</string>
|
||||
<string name="enable_search_history_title">Претходни пребарувања</string>
|
||||
<string name="enable_search_history_summary">Зачувајте ги пребарувањата локално</string>
|
||||
<string name="enable_watch_history_title">Историја и зачувано</string>
|
||||
<string name="enable_watch_history_title">Историја на гледање</string>
|
||||
<string name="enable_watch_history_summary">Зачувајте ја листата на гледани видеа</string>
|
||||
<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_hold_to_append_title">Прикажи „задржи за прикачување“</string>
|
||||
<string name="show_hold_to_append_title">Прикажи го советот: „Задржи за додавање во редослед“</string>
|
||||
<string name="show_hold_to_append_summary">Покажи совет при притискање на позадината или кога скок копчето е притиснато на видео \"Детали:\"</string>
|
||||
<string name="unsupported_url">Неподдржана URL врска</string>
|
||||
<string name="default_content_country_title">Земја на прикажани видеа</string>
|
||||
<string name="content_language_title">Стандарден јазик на содржина</string>
|
||||
<string name="settings_category_player_title">Плеер</string>
|
||||
<string name="settings_category_player_behavior_title">Однесување</string>
|
||||
<string name="settings_category_video_audio_title">Видео и Звук</string>
|
||||
<string name="settings_category_history_title">Историја</string>
|
||||
<string name="settings_category_video_audio_title">Видео и аудио</string>
|
||||
<string name="settings_category_history_title">Историја и кеш</string>
|
||||
<string name="settings_category_appearance_title">Изглед</string>
|
||||
<string name="settings_category_debug_title">Дебагирање</string>
|
||||
<string name="background_player_playing_toast">Пуштено во позадина</string>
|
||||
@ -113,7 +113,7 @@
|
||||
<string name="network_error">Мрежна грешка</string>
|
||||
<string name="could_not_load_thumbnails">Не можеа да се прочитаат сите сликички</string>
|
||||
<string name="parsing_error">Не може да се прочита страната</string>
|
||||
<string name="content_not_available">Содржината е недостапна</string>
|
||||
<string name="content_not_available">Содржината не е достапна</string>
|
||||
<string name="could_not_setup_download_menu">Неуспешно поставување на менито за превземања</string>
|
||||
<string name="app_ui_crash">Се сруши Апп./Интерфејс</string>
|
||||
<string name="player_stream_failure">Неуспех при пуштање на видеото</string>
|
||||
@ -122,21 +122,21 @@
|
||||
<string name="external_player_unsupported_link_type">Надворешните плеери не поддржуваат вакви врски</string>
|
||||
<string name="video_streams_empty">Нема видео стримови</string>
|
||||
<string name="audio_streams_empty">Нема аудио стримови</string>
|
||||
<string name="invalid_directory">Невалидна локација</string>
|
||||
<string name="invalid_source">Невалиден извор на датотека/содржина</string>
|
||||
<string name="invalid_file">Датотеката не постои или се потребни привилегии за достапност</string>
|
||||
<string name="file_name_empty_error">Датотеката не може да биде празна</string>
|
||||
<string name="invalid_directory">Таква папка не постои</string>
|
||||
<string name="invalid_source">Таков фајл / содржина не постои</string>
|
||||
<string name="invalid_file">Фајлот не постои или недостасува дозвола за негово читање или допишување</string>
|
||||
<string name="file_name_empty_error">Името на фајлот не може да биде празно</string>
|
||||
<string name="error_occurred_detail">Настана проблем: %1$s</string>
|
||||
<string name="sorry_string">Извинете, не требаше да се случи тоа.</string>
|
||||
<string name="error_report_button_text">Извести за грешката по е-пошта</string>
|
||||
<string name="error_snackbar_message">Извинете, настана проблем.</string>
|
||||
<string name="error_snackbar_action">ИЗВЕШТАЈ</string>
|
||||
<string name="error_report_button_text">Пријави ја грешката по мејл</string>
|
||||
<string name="error_snackbar_message">Извинете, нешто тргна наопаку.</string>
|
||||
<string name="error_snackbar_action">Извештај</string>
|
||||
<string name="what_device_headline">Инфо:</string>
|
||||
<string name="what_happened_headline">Што се случи:</string>
|
||||
<string name="info_labels">Што:\\nБарање:\\nЈазик на Содрж.:\\nУслуга:\\nGMT Час:\\nПакет:\\nВерзија:\\nВерз. на ОС:</string>
|
||||
<string name="info_labels">What:\\nRequest:\\nContent Language:\\nContent Country:\\nApp Language:\\nService:\\nGMT Time:\\nPackage:\\nVersion:\\nOS version:</string>
|
||||
<string name="your_comment">Ваш коментар (на Англиски):</string>
|
||||
<string name="error_details_headline">Детали:</string>
|
||||
<string name="detail_thumbnail_view_description">Сликичка за преглед на видеото</string>
|
||||
<string name="detail_thumbnail_view_description">Емитување на видео, времетраење</string>
|
||||
<string name="detail_uploader_thumbnail_view_description">Икона од аватарот на објавителот</string>
|
||||
<string name="detail_likes_img_view_description">Допаѓања</string>
|
||||
<string name="detail_dislikes_img_view_description">Недопаѓања</string>
|
||||
@ -146,9 +146,9 @@
|
||||
<string name="video">Видео</string>
|
||||
<string name="audio">Звук</string>
|
||||
<string name="retry">Пробај повторно</string>
|
||||
<string name="short_thousand">K</string>
|
||||
<string name="short_thousand">илјади</string>
|
||||
<string name="short_million">M</string>
|
||||
<string name="short_billion">B</string>
|
||||
<string name="short_billion">милијарди</string>
|
||||
<string name="no_subscribers">Нема зачленети</string>
|
||||
<plurals name="subscribers">
|
||||
<item quantity="one">%s зачленет</item>
|
||||
@ -179,7 +179,7 @@
|
||||
<string name="msg_running_detail">Притисни за детали</string>
|
||||
<string name="msg_wait">Почекајте…</string>
|
||||
<string name="msg_copied">Копирано во привремена меморија</string>
|
||||
<string name="no_available_dir">Одберете достапна локација за превземања</string>
|
||||
<string name="no_available_dir">Ве молиме, определете папка за преземања подоцна во поставките</string>
|
||||
<string name="msg_popup_permission">Оваа привилегија е потребна за
|
||||
\nотворање во подпрозорче</string>
|
||||
<string name="title_activity_recaptcha">reCAPTCHA Предизвик</string>
|
||||
@ -193,14 +193,14 @@
|
||||
<string name="title_activity_about">За NewPipe</string>
|
||||
<string name="title_licenses">Лиценци од трети страни</string>
|
||||
<string name="copyright" formatted="true">© %1$s од %2$s под %3$s</string>
|
||||
<string name="tab_about">За апликацијата</string>
|
||||
<string name="tab_about">За апликацијата и често поставувани прашања</string>
|
||||
<string name="tab_licenses">Лиценци</string>
|
||||
<string name="app_description">Слободна, лесна YouTube апликација за Андроид.</string>
|
||||
<string name="contribution_title">Допринеси</string>
|
||||
<string name="contribution_encouragement">Ако имаш идеи за; превод, дизајн, чистење или големи промени во кодот - помошта е секогаш добредојдена. Што повеќе се работи, подобра е апликацијата!</string>
|
||||
<string name="view_on_github">Види на GitHub</string>
|
||||
<string name="donation_title">Донирај</string>
|
||||
<string name="donation_encouragement">NewPipe се гради од волонтери кои го вложуваат слободното време за важето задоволство. Време е да им возвратиме за тие да можат да ја подобрат апликацијата, уживајќи во топло кафе.</string>
|
||||
<string name="donation_encouragement">NewPipe е разработуван од доброволци, кои го вложуваат нивното слободно време за да ви го овозможат најдоброто корисничко искуство. Возвратете им на програмерите со една чаша кафе, за да можат да го направат NewPipe уште подобар.</string>
|
||||
<string name="give_back">Возврати</string>
|
||||
<string name="website_title">Вебсајт</string>
|
||||
<string name="website_encouragement">За повеќе информации и новости за NewPipe посети ја вебстраната.</string>
|
||||
@ -216,21 +216,21 @@
|
||||
<string name="kiosk_page_summary">Киоск</string>
|
||||
<string name="channel_page_summary">Канали</string>
|
||||
<string name="select_a_channel">Одбери канал</string>
|
||||
<string name="no_channel_subscribed_yet">Не сте член на канал</string>
|
||||
<string name="no_channel_subscribed_yet">Во моментов немате претплати</string>
|
||||
<string name="select_a_kiosk">Одбери киоск</string>
|
||||
<string name="export_complete_toast">Успешно извезување</string>
|
||||
<string name="import_complete_toast">Успешно внесување</string>
|
||||
<string name="export_complete_toast">Извезувањето беше успешно</string>
|
||||
<string name="import_complete_toast">Увезувањето беше успешно</string>
|
||||
<string name="no_valid_zip_file">Нема валиден ZIP фајл</string>
|
||||
<string name="could_not_import_all_files">Предупредување: Не се внесени сите датотеки.</string>
|
||||
<string name="override_current_data">Ова ќе ги избрише моменталните поставки.</string>
|
||||
<string name="trending">Популарни</string>
|
||||
<string name="top_50">Топ 50</string>
|
||||
<string name="new_and_hot">Нови и жешки</string>
|
||||
<string name="new_and_hot">Ново и популарно</string>
|
||||
<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>
|
||||
@ -293,11 +293,11 @@
|
||||
<string name="watch_history_deleted">Избришана е историјата на гледаност.</string>
|
||||
<string name="clear_search_history_title">Избриши историја на пребарувања</string>
|
||||
<string name="clear_search_history_summary">Ја брише историјата на пребарувања</string>
|
||||
<string name="delete_search_history_alert">Избриши ја целата историја на пребарувања.</string>
|
||||
<string name="search_history_deleted">Избришана е историјата на пребарувања.</string>
|
||||
<string name="delete_search_history_alert">Да се избрише сета историја на пребарувања?</string>
|
||||
<string name="search_history_deleted">Историјата на пребарувања е избришана.</string>
|
||||
<string name="no_streams_available_download">Нема стримови за симнување</string>
|
||||
<string name="one_item_deleted">1 ставка избришана.</string>
|
||||
<string name="app_license">NewPipe е „copyleft“ слободен софтвер: Можеш да ја користиш, истражуваш и подобруваш по твоја желба. Можеш да ја редистрибуираш и/или да ја промениш под условите на GNU GPL лиценцата, публикувана од фондацијата FSF - или верзија 3 од лиценцата, или (по можност) понова верзија.</string>
|
||||
<string name="app_license">NewPipe е „copyleft“ слободен софтвер: Можеш да го користиш, истражуваш и подобруваш по твоја желба. Можеш да го редистрибуираш и/или да го промениш под условите на GNU GPL лиценцата, објавена од фондацијата FSF - или верзија 3 од лиценцата, или (по можност) понова верзија.</string>
|
||||
<string name="import_settings">Дали сакаш да се внесат и подесувањата?</string>
|
||||
<string name="preferred_open_action_settings_title">Претпочитана акција за „отворање“</string>
|
||||
<string name="preferred_open_action_settings_summary">Стандардна акција при отворање видеа — %s</string>
|
||||
@ -332,8 +332,8 @@
|
||||
<string name="app_update_notification_channel_name">Известување за ажурирање на апликација</string>
|
||||
<string name="app_update_notification_channel_description">Известување за нова NewPipe верзија</string>
|
||||
<string name="download_to_sdcard_error_title">Надворешната меморија е недостапна</string>
|
||||
<string name="download_to_sdcard_error_message">Превземањето на надворешната SD картичка не е можно. Ресетирајте ја локацијата на папката за преземање \?</string>
|
||||
<string name="subscribers_count_not_available">Бројот на зачленети не е достапен</string>
|
||||
<string name="download_to_sdcard_error_message">Преземањето на SD картичка не е возможно. Да се ресетира локацијата на папката за преземања ?</string>
|
||||
<string name="subscribers_count_not_available">Бројот на претплатници не е достапен</string>
|
||||
<string name="main_page_content_summary">Кои јазичиња се прикажани на главната страница</string>
|
||||
<string name="updates_setting_title">Ажурирања</string>
|
||||
<string name="updates_setting_description">Покажи известување за навремено ажурирање на апликацијата кога е достапна нова верзија</string>
|
||||
@ -347,21 +347,21 @@
|
||||
<string name="paused">Паузирано</string>
|
||||
<string name="queued">Во ред за чекање</string>
|
||||
<string name="enqueue">Редица</string>
|
||||
<string name="permission_denied">Акција одбиена од системот</string>
|
||||
<string name="permission_denied">Дејствието беше одбиено од системот</string>
|
||||
<string name="download_failed">преземањето не успеа</string>
|
||||
<string name="generate_unique_name">Генерирај уникатно име</string>
|
||||
<string name="generate_unique_name">Создај уникатно име</string>
|
||||
<string name="overwrite">Презапиши</string>
|
||||
<string name="download_already_running">Во тек е преземање со ова име</string>
|
||||
<string name="show_error">Покажи грешка</string>
|
||||
<string name="error_path_creation">Папката не може да се создаде</string>
|
||||
<string name="error_file_creation">Датотеката не може да се создаде</string>
|
||||
<string name="error_ssl_exception">Безбедна врска не успеа</string>
|
||||
<string name="error_unknown_host">Не може да се пронајде серверот</string>
|
||||
<string name="error_connect_host">Не може да се поврзе со серверот</string>
|
||||
<string name="error_path_creation">Избраната папка не може да се создаде</string>
|
||||
<string name="error_file_creation">Фајлот не може да се создаде</string>
|
||||
<string name="error_ssl_exception">Неуспешно воспоставување на сигурна врска</string>
|
||||
<string name="error_unknown_host">Серверот не може да се пронајде</string>
|
||||
<string name="error_connect_host">Неуспешно поврзување со серверот</string>
|
||||
<string name="error_http_no_content">Серверот не испраќа податоци</string>
|
||||
<string name="error_http_unsupported_range">Серверот не прифаќа преземања со повеќе навои, обидете се со @string/msg_threads = 1</string>
|
||||
<string name="error_http_unsupported_range">Серверот не прифаќа преземања со повеќе нишки, обидете се со @string/msg_threads = 1</string>
|
||||
<string name="error_http_not_found">Не е најдено</string>
|
||||
<string name="stop">Стоп</string>
|
||||
<string name="stop">Престани</string>
|
||||
<string name="max_retry_msg">Максимални обиди</string>
|
||||
<string name="max_retry_desc">Максимален број обиди пред откажување на преземањето</string>
|
||||
<string name="pause_downloads_on_mobile_desc">Корисно при префрлување на мобилни податоци, иако некои преземања не можат да бидат прекинати</string>
|
||||
@ -370,7 +370,7 @@
|
||||
<string name="show_comments_title">Прикажи коментари</string>
|
||||
<string name="autoplay_title">Автоматско пуштање</string>
|
||||
<string name="no_comments">Нема коментари</string>
|
||||
<string name="error_unable_to_load_comments">Не може да се вчитаат коментарите</string>
|
||||
<string name="error_unable_to_load_comments">Грешка при вчитувањето на коментарите</string>
|
||||
<string name="close">Затвори</string>
|
||||
<string name="enable_playback_resume_title">Продолжи со репродукција</string>
|
||||
<string name="enable_playback_resume_summary">Враќање на последната позиција на репродукција</string>
|
||||
@ -378,16 +378,16 @@
|
||||
<string name="missing_file">Дадотеката е преместена или избришана</string>
|
||||
<string name="overwrite_unrelated_warning">Ддотека со ова име веќе постои</string>
|
||||
<string name="overwrite_finished_warning">Преземената дадотека со ова име веќе постои</string>
|
||||
<string name="error_postprocessing_stopped">NewPipe беше затворен додека работеше на датотеката</string>
|
||||
<string name="error_postprocessing_stopped">NewPipe беше затворен додека го обработуваше фајлот</string>
|
||||
<string name="error_insufficient_storage_left">Не останува простор на уредот</string>
|
||||
<string name="error_timeout">Истечено време за поврзување</string>
|
||||
<string name="confirm_prompt">Дали си сигурен\?</string>
|
||||
<string name="enable_queue_limit">Ограничи ја редицата за преземање</string>
|
||||
<string name="confirm_prompt">Сакате да ја исчистите историјата на преземања или да ги избришете сите преземени фајлови?</string>
|
||||
<string name="enable_queue_limit">Ограничи ја редицата за преземања</string>
|
||||
<string name="enable_queue_limit_desc">Едно преземање ќе работи истовремено</string>
|
||||
<string name="start_downloads">Започи со преземања</string>
|
||||
<string name="start_downloads">Започни преземања</string>
|
||||
<string name="pause_downloads">Паузирај преземања</string>
|
||||
<string name="downloads_storage_ask_summary">Вие ќе добиете прашање каде да го зачувате секое преземање</string>
|
||||
<string name="downloads_storage_use_saf_title">Користи SAF</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">Рамката за пристап до складирање овозможува преземања во надворешна SD-картичка.
|
||||
\nЗабелешка: некои уреди не се поддржани.</string>
|
||||
<string name="show_comments_summary">Исклучете за да ги скриете коментарите</string>
|
||||
@ -398,4 +398,328 @@
|
||||
<string name="peertube_instance_url_summary">Одберете ги вашите омилени PeerTube пример</string>
|
||||
<string name="peertube_instance_url_help">Пронајдете ги примерите кои ви се допаѓаат на %s</string>
|
||||
<string name="peertube_instance_add_title">Додадете пример</string>
|
||||
<string name="recent">Скорешни</string>
|
||||
<string name="enqueue_stream">Додај во редослед</string>
|
||||
<string name="feed_show_partially_watched">Делумно изгледани</string>
|
||||
<plurals name="watching">
|
||||
<item quantity="one">%s гледач</item>
|
||||
<item quantity="other">%s гледачи</item>
|
||||
</plurals>
|
||||
<string name="show_less">Прикажи помалку</string>
|
||||
<string name="main_tabs_position_summary">Премести ги основниот селектор на јазичиња најдолу</string>
|
||||
<string name="main_tabs_position_title">Позиција на основните јазичиња</string>
|
||||
<string name="show_channel_details">Прикажи информации за каналот</string>
|
||||
<string name="remove_watched_popup_yes_and_partially_watched_videos">Да, како и делумно изгледаните видеа</string>
|
||||
<string name="audio_track_present_in_video">Аудио снимка треба да е веќе присутна во овој стрим</string>
|
||||
<plurals name="listening">
|
||||
<item quantity="one">%s слушател</item>
|
||||
<item quantity="other">%s слушатели</item>
|
||||
</plurals>
|
||||
<string name="local">Местно</string>
|
||||
<string name="recently_added">Скоро додадено</string>
|
||||
<string name="error_insufficient_storage">Нема доволно слободна меморија на уредот</string>
|
||||
<string name="feed_notification_loading">Вчитување на фидот…</string>
|
||||
<string name="metadata_category">Категорија</string>
|
||||
<string name="no_video_streams_available_for_external_players">Нема достапни стримови за надворешни плеери</string>
|
||||
<string name="open_with">Отвори со</string>
|
||||
<string name="mark_as_watched">Обележи како гледано</string>
|
||||
<string name="night_theme_title">Ноќен режим</string>
|
||||
<string name="clear_queue_confirmation_description">Активниот редослед на емитувања ќе биде заменет</string>
|
||||
<string name="clear_download_history">Исчисти ја историјата на преземања</string>
|
||||
<string name="local_search_suggestions">Местни предлози за пребарување</string>
|
||||
<string name="peertube_instance_add_help">Внесете URL адреса на инстанцата</string>
|
||||
<string name="error_postprocessing_failed">Неуспешна постобработка</string>
|
||||
<string name="remove_duplicates_message">Да се отстранат сите стримови дупликати од оваа плејлиста?</string>
|
||||
<string name="feed_create_new_group_button_title">Нова група</string>
|
||||
<string name="feed_load_error_account_info">Неуспешно вчитување на новинска лента за „%s“.</string>
|
||||
<string name="feed_show_hide_streams">Прикажи / скриј стримови</string>
|
||||
<string name="private_content">Оваа содржина е приватна, така што не може да биде емитувана или преземена од страна на NewPipe.</string>
|
||||
<string name="service_provides_reason">%s ја посочува следната причина:</string>
|
||||
<string name="featured">Истакнато</string>
|
||||
<string name="radio">Радио</string>
|
||||
<string name="auto_device_theme_title">Автоматски (режим на уредот)</string>
|
||||
<string name="download_has_started">Преземањето започна</string>
|
||||
<string name="metadata_privacy">Приватност</string>
|
||||
<string name="metadata_age_limit">Возрасна граница</string>
|
||||
<string name="metadata_privacy_unlisted">Скриено</string>
|
||||
<string name="metadata_privacy_private">Приватно</string>
|
||||
<string name="progressive_load_interval_exoplayer_default">ExoPlayer стандардно</string>
|
||||
<string name="channel_tab_tracks">Снимки</string>
|
||||
<string name="channel_tab_videos">Видеа</string>
|
||||
<string name="channel_tab_shorts">Кратки видеа</string>
|
||||
<string name="channel_tab_livestreams">Во живо</string>
|
||||
<string name="image_quality_high">Висок квалитет</string>
|
||||
<string name="question_mark">\?</string>
|
||||
<string name="share_playlist">Сподели ја плејлистата</string>
|
||||
<string name="share_playlist_with_titles">Сподели со наслови</string>
|
||||
<string name="show_more">Прикажи повеќе</string>
|
||||
<string name="comments_are_disabled">Коментарите се исклучени</string>
|
||||
<string name="tablet_mode_title">Таблет-режим</string>
|
||||
<string name="on">Вклучено</string>
|
||||
<string name="overwrite_failed">неуспешно презапишување на фајлот</string>
|
||||
<string name="most_liked">Најпопуларни</string>
|
||||
<string name="no_streams">Нема стримови</string>
|
||||
<string name="main_page_content_swipe_remove">Лизнете елементи за да ги избришете</string>
|
||||
<string name="notification_scale_to_square_image_title">Исечи ја сликичката на сооднос 1:1</string>
|
||||
<string name="saved_tabs_invalid_json">Грешка при читањето на зачуваните јазичиња, ќе бидат користени стандардните јазичиња</string>
|
||||
<string name="permission_display_over_apps">Дај дозвола за прикажување врз други апликации</string>
|
||||
<string name="error_report_notification_title">NewPipe откри грешка, допрете за да ја пријавите</string>
|
||||
<string name="drawer_header_description">Вклучување на услуги, тековно избрани:</string>
|
||||
<string name="recaptcha_solve">Решете</string>
|
||||
<string name="select_a_playlist">Изберете плејлиста</string>
|
||||
<string name="title_activity_play_queue">Емитувај го редоследот</string>
|
||||
<string name="remove_watched_popup_warning">Видеата кои биле гледани пред и откако биле додадени во плејлистата ќе бидат отстранети.\nСигурни ли сте? Оваа промена е трајна!</string>
|
||||
<plurals name="minutes">
|
||||
<item quantity="one">%d минута</item>
|
||||
<item quantity="other">%d минути</item>
|
||||
</plurals>
|
||||
<plurals name="feed_group_dialog_selection_count">
|
||||
<item quantity="one">%d избран</item>
|
||||
<item quantity="other">%d избрани</item>
|
||||
</plurals>
|
||||
<string name="no_appropriate_file_manager_message">Не е пронајден соодветен менаџер за фајлови за ова дејствие.\nВе молиме инсталирајте менаџер за фајлови (анг. file manager) или обидете се да деактивирате „%s“ во поставките за преземања</string>
|
||||
<string name="audio_track_type_secondary">секундарна</string>
|
||||
<string name="channel_tab_channels">Канали</string>
|
||||
<string name="channel_tab_playlists">Плејлисти</string>
|
||||
<string name="channel_tab_albums">Албуми</string>
|
||||
<string name="channel_tab_about">За каналот</string>
|
||||
<string name="show_channel_tabs">Картички на канали</string>
|
||||
<string name="toggle_screen_orientation">Смени ја ориентацијата на екранот</string>
|
||||
<string name="next_stream">Следен стрим</string>
|
||||
<string name="replay">Пушти повторно</string>
|
||||
<string name="more_options">Повеќе опции</string>
|
||||
<string name="duration">Времетраење</string>
|
||||
<string name="rewind">Премотување наназад</string>
|
||||
<string name="forward">Премотување нанапред</string>
|
||||
<string name="image_quality_title">Квалитет на слика</string>
|
||||
<string name="image_quality_none">Не вчитувај слики</string>
|
||||
<string name="share_playlist_with_list">Сподели список со линкови</string>
|
||||
<plurals name="replies">
|
||||
<item quantity="one">%s одговор</item>
|
||||
<item quantity="other">%s одговори</item>
|
||||
</plurals>
|
||||
<string name="ignore_hardware_media_buttons_title">Игнорирај хардверски копчиња за медиуми</string>
|
||||
<string name="ignore_hardware_media_buttons_summary">Корисно, на пример, ако користите слушалки со скршени физички копчиња</string>
|
||||
<string name="start_main_player_fullscreen_title">Стартирајте го основниот плеер на цел екран</string>
|
||||
<string name="infinite_videos">∞ видеа</string>
|
||||
<string name="notification_action_buffering">Буферирање</string>
|
||||
<string name="notification_action_nothing">Ништо</string>
|
||||
<string name="notification_colorize_title">Обој го известувањето</string>
|
||||
<string name="notification_action_repeat">Повтори</string>
|
||||
<string name="notification_action_shuffle">Измешај</string>
|
||||
<string name="paid_content">Оваа содржина е достапна само за оние корисници кои платиле, така што не може да биде емитувана или преземена од страна на NewPipe.</string>
|
||||
<string name="georestricted_content">Оваа содржина не е достапна во Вашата држава.</string>
|
||||
<string name="youtube_music_premium_content">Ова видео е достапно само за претплатници на YouTube Music Premium, така што не може да биде емитувано или преземено од страна на NewPipe.</string>
|
||||
<string name="metadata_thumbnails">Сликички</string>
|
||||
<string name="description_select_enable">Овозможи го селектирањето на текст во описот</string>
|
||||
<string name="account_terminated">Акаунтот е укинат</string>
|
||||
<string name="metadata_uploader_avatars">Аватари на објавувачот</string>
|
||||
<string name="metadata_privacy_public">Јавно</string>
|
||||
<string name="metadata_subscribers">Претплатници</string>
|
||||
<string name="detail_pinned_comment_view_description">Закачен коментар</string>
|
||||
<string name="detail_heart_img_view_description">Обележан со срце од авторот</string>
|
||||
<string name="open_website_license">Отвори ја веб-страницата</string>
|
||||
<string name="off">Исклучено</string>
|
||||
<string name="notifications_disabled">Известувањата се оневозможени</string>
|
||||
<string name="enumeration_comma">,</string>
|
||||
<string name="unknown_quality">Непознат квалитет</string>
|
||||
<string name="download_already_pending">Веќе има преземање со тоа име кое претстои</string>
|
||||
<string name="error_download_resource_gone">Неуспешно враќање на преземањето</string>
|
||||
<plurals name="hours">
|
||||
<item quantity="one">%d час</item>
|
||||
<item quantity="other">%d часа</item>
|
||||
</plurals>
|
||||
<string name="delete_downloaded_files">Избриши ги преземените фајлови</string>
|
||||
<string name="feed_show_upcoming">Претстојни</string>
|
||||
<string name="settings_category_exoplayer_title">Поставки за ExoPlayer</string>
|
||||
<string name="share_playlist_content_details">%1$s \n%2$s</string>
|
||||
<string name="delete_downloaded_files_confirm">Избриши ги сите преземени фајлови од дискот?</string>
|
||||
<string name="feed_use_dedicated_fetch_method_summary">Достапно за некои услуги, обично е многу побрзо но можно е да врати ограничено количество видеа и честопати нецелосни информации (пр. нема времетраење, вид на видеото, дали е во живо)</string>
|
||||
<string name="notification_action_2_title">Копче за трето дејствие</string>
|
||||
<plurals name="days">
|
||||
<item quantity="one">%d ден</item>
|
||||
<item quantity="other">%d дена</item>
|
||||
</plurals>
|
||||
<string name="feed_show_watched">Целосно изгледани</string>
|
||||
<string name="audio_track_name">%1$s%2$s</string>
|
||||
<string name="audio_track_type_original">оригинална</string>
|
||||
<string name="audio_track_type_dubbed">натсинхронизирана</string>
|
||||
<string name="audio_track_type_descriptive">описна</string>
|
||||
<string name="prefer_original_audio_title">Претпочитај ја оригиналната аудио снимка</string>
|
||||
<string name="volume">Сила на звукот</string>
|
||||
<string name="checking_updates_toast">Проверка за ажурирања…</string>
|
||||
<string name="app_language_title">Јазик на интерфејсот</string>
|
||||
<string name="enqueued_next">Следното беше додадено во редослед</string>
|
||||
<string name="faq_title">Често поставувани прашања</string>
|
||||
<string name="faq">Отвори ја веб-страницата</string>
|
||||
<string name="content_not_supported">Оваа содржина сѐ не е поддржана од NewPipe.\n\nСе надеваме дека ќе биде поддржана во некоја идна верзија.</string>
|
||||
<string name="right_gesture_control_title">Дејствие со гестикулација оддесно</string>
|
||||
<string name="settings_category_player_notification_title">Известување на плеерот</string>
|
||||
<string name="downloads_storage_ask_title">Прашај ме за папка за преземање</string>
|
||||
<string name="image_quality_low">Низок квалитет</string>
|
||||
<string name="metadata_language">Јазик</string>
|
||||
<string name="always_use_exoplayer_set_output_surface_workaround_title">Секогаш користете го заобиколувањето на опцијата на површината на видео-излезот на ExoPlayer\n(Англиски: Always use ExoPlayer\'s video output surface setting workaround)</string>
|
||||
<string name="peertube_instance_add_https_only">Поддржани се само HTTPS линкови</string>
|
||||
<string name="peertube_instance_add_exists">Инстанцата веќе постои</string>
|
||||
<plurals name="deleted_downloads_toast">
|
||||
<item quantity="one">Избришано е %1$s преземање</item>
|
||||
<item quantity="other">Избришани се %1$s преземања</item>
|
||||
</plurals>
|
||||
<plurals name="download_finished_notification">
|
||||
<item quantity="one">Преземањето заврши</item>
|
||||
<item quantity="other">%s преземања се готови</item>
|
||||
</plurals>
|
||||
<string name="watch_history_states_deleted">Позициите за репродукција се избришани</string>
|
||||
<string name="fast_mode">Брз режим</string>
|
||||
<string name="copy_for_github">Ископирај го форматираниот извештај</string>
|
||||
<string name="reset_settings_summary">Ресетирај ги сите поставки до стандардните вредности</string>
|
||||
<string name="error_progress_lost">Прогресот е загубен, затоа што фајлот е избришан</string>
|
||||
<string name="pause_downloads_on_mobile">Прекини преземања на измерени мрежи</string>
|
||||
<string name="downloads_storage_use_saf_summary_api_29">Почнувајќи од Android 10, се поддржува само „Storage Access Framework“</string>
|
||||
<string name="choose_instance_prompt">Одберете инстанца</string>
|
||||
<string name="systems_language">Стандарден јазик на уредот</string>
|
||||
<string name="remove_watched">Отстрани ги гледаните</string>
|
||||
<string name="remove_watched_popup_title">Да се отстранат гледаните видеа?</string>
|
||||
<string name="remove_duplicates">Отстрани ги дупликатите</string>
|
||||
<string name="remove_duplicates_title">Да се отстранат дупликатите?</string>
|
||||
<string name="feed_group_dialog_empty_name">Името на групата е празно</string>
|
||||
<string name="feed_new_items">Нови видеа во лентата со новости</string>
|
||||
<string name="feed_group_dialog_select_subscriptions">Изберете претплати</string>
|
||||
<string name="feed_group_dialog_empty_selection">Нема избрани претплати</string>
|
||||
<string name="feed_fetch_channel_tabs">Извлечи јазичиња (анг. tabs) на канали</string>
|
||||
<string name="feed_use_dedicated_fetch_method_enable_button">Овозможи брз режим</string>
|
||||
<string name="feed_use_dedicated_fetch_method_disable_button">Оневозможи брз режим</string>
|
||||
<string name="feed_fetch_channel_tabs_summary">Јазичиња за извлекување кога новинската лента се ажурира. Оваа опција нема ефект доколку каналот се ажурира со брз режим.</string>
|
||||
<string name="playlist_page_summary">Страница на плејлистата</string>
|
||||
<string name="channel_created_by">Создаден од %s</string>
|
||||
<string name="video_detail_by">Од %s</string>
|
||||
<string name="soundcloud_go_plus_content">Ова е песна на SoundCloud Go+ (барем во Вашата држава), така што не може да биде емитувана или преземена од страна на NewPipe.</string>
|
||||
<string name="night_theme_summary">Изберете го Вашиот омилен ноќен режим — %s</string>
|
||||
<string name="select_night_theme_toast">Можете да го изберете Вашиот омилен ноќен режим подолу</string>
|
||||
<string name="metadata_tags">Ознаки</string>
|
||||
<string name="metadata_licence">Лиценца</string>
|
||||
<string name="metadata_support">Поддршка</string>
|
||||
<string name="metadata_host">Домаќин</string>
|
||||
<string name="metadata_subchannel_avatars">Аватари на подканали</string>
|
||||
<string name="metadata_avatars">Аватари</string>
|
||||
<string name="metadata_banners">Банери</string>
|
||||
<string name="select_quality_external_players">Избери квалитет за надворешни плеери</string>
|
||||
<string name="select_audio_track_external_players">Избери аудио снимка за надворешни плеери</string>
|
||||
<string name="get_notified">Добивај известувања</string>
|
||||
<string name="sort">Подреди</string>
|
||||
<string name="you_successfully_subscribed">Се претплативте на овој канал</string>
|
||||
<string name="toggle_all">Вклучи ги сите</string>
|
||||
<string name="show_channel_tabs_summary">Кои картички се прикажани на страниците на каналите</string>
|
||||
<string name="open_play_queue">Отвори редослед на емитувања</string>
|
||||
<string name="toggle_fullscreen">Пушти на цел екран</string>
|
||||
<string name="previous_stream">Претходен стрим</string>
|
||||
<string name="image_quality_medium">Среден квалитет</string>
|
||||
<string name="msg_failed_to_copy">Неуспешно копирање во клипборд</string>
|
||||
<string name="yes">Да</string>
|
||||
<string name="no">Не</string>
|
||||
<string name="search_showing_result_for">Прикажани резулати за: %s</string>
|
||||
<string name="notification_scale_to_square_image_summary">Исечи ја сликичката на видеото прикажана во известувањето од 16:9 до 1:1 сооднос</string>
|
||||
<string name="notification_action_0_title">Копче за прво дејствие</string>
|
||||
<string name="notification_action_1_title">Копче за второ дејствие</string>
|
||||
<string name="notification_action_3_title">Копче за четврто дејствие</string>
|
||||
<string name="notification_action_4_title">Копче за петто дејствие</string>
|
||||
<string name="notification_actions_summary_android13">Уредете го секое дејствие за известување подолу, допирајќи на него. Првите три дејствија (пушти/паузирај, претходно и следно) се поставени од системот и не можат да се приспособат.</string>
|
||||
<string name="notification_colorize_summary">Дозволете Android да ја приспособи бојата на известувањата според главната боја во сликичката (имајте предвид дека ова не е достапно на сите уреди)</string>
|
||||
<string name="crash_the_player">Сруши го плеерот</string>
|
||||
<string name="progressive_load_interval_title">Големина на интервалот на вчитување при емитување</string>
|
||||
<string name="progressive_load_interval_summary">Променето ја големината на интервалот на вчитување при емитување на прогресивни содржини (во моментов %s). Пониска вредност ,пже да го забрза нивното почетно вчитување</string>
|
||||
<string name="clear_queue_confirmation_summary">Префрлањето од еден плеер на друг може да го замени Вашиот редослед</string>
|
||||
<string name="show_description_title">Прикажи опис</string>
|
||||
<string name="show_description_summary">Исклучете за да го скриете описот на видеото и дополнителните информации</string>
|
||||
<string name="show_meta_info_title">Прикажи метаподатоци</string>
|
||||
<string name="prefer_descriptive_audio_title">Претпочитај описни аудио снимки</string>
|
||||
<string name="prefer_original_audio_summary">Ја избира оригиналната аудио снимка независно од јазикот</string>
|
||||
<string name="auto_queue_toggle">Автоматско емитување на следното видео</string>
|
||||
<string name="left_gesture_control_summary">Изберете гестикулација за левата половина од екранот на плеерот</string>
|
||||
<string name="brightness">Светлина</string>
|
||||
<string name="remote_search_suggestions">Предлози за оддалечено пребарување</string>
|
||||
<string name="restore_defaults">Врати ги стандардните</string>
|
||||
<string name="restore_defaults_confirmation">Дали сакате да ги вратите стандардните поставки?</string>
|
||||
<string name="comments_tab_description">Коментари</string>
|
||||
<string name="related_items_tab_description">Поврзани работи</string>
|
||||
<string name="description_tab_description">Опис</string>
|
||||
<string name="import_subscriptions_hint">Увези или извези претплати од менито со три точки</string>
|
||||
<string name="subtitle_activity_recaptcha">Стиснете на „Готово“ кога ќе решите</string>
|
||||
<string name="reset_settings_title">Ресетирај ги поставките</string>
|
||||
<string name="downloads_storage_ask_summary_no_saf_notice">Ќе бидете прашани каде да го зачувате секое преземање</string>
|
||||
<plurals name="seconds">
|
||||
<item quantity="one">%d секунда</item>
|
||||
<item quantity="other">%d секунди</item>
|
||||
</plurals>
|
||||
<string name="feed_groups_header_title">Група од канали</string>
|
||||
<string name="feed_oldest_subscription_update">Последно ажурирање на фидот: %s</string>
|
||||
<string name="feed_subscription_not_loaded_count">Не се вчитани: %d</string>
|
||||
<string name="feed_processing_message">Обработка на фидот…</string>
|
||||
<string name="feed_group_dialog_delete_message">Да се избрише оваа група?</string>
|
||||
<string name="settings_category_feed_title">Лента со новости</string>
|
||||
<string name="feed_update_threshold_title">Праг за ажурирање на лентата со новости</string>
|
||||
<string name="feed_update_threshold_option_always_update">Секогаш ажурирај</string>
|
||||
<string name="feed_load_error">Грешка при вчитувањето на лентата со новости</string>
|
||||
<string name="feed_load_error_fast_unknown">Брзиот режим на новинската лента не доставува повеќе информации за ова.</string>
|
||||
<string name="show_thumbnail_title">Прикажи ја сликичката</string>
|
||||
<string name="show_thumbnail_summary">Искористи ја сликичката и за позадината на заклучениот екран и за известувањата</string>
|
||||
<string name="chapters">Поглавја</string>
|
||||
<string name="no_app_to_open_intent">Нема апликација на уредот што може да го отвори овој фајл</string>
|
||||
<string name="night_theme_available">Оваа опција е достапна само ако %s е избрано за режим</string>
|
||||
<string name="description_select_disable">Оневозможи го селектирањето на текст во описот</string>
|
||||
<string name="unknown_format">Непознат формат</string>
|
||||
<string name="unknown_audio_track">Непозната</string>
|
||||
<string name="play">Пушти</string>
|
||||
<string name="notification_actions_summary">Уредете го секое дејствие за известување подолу допирајќи на него. Изберете до три од нив за да се прикажат во компактното известување со користење на полињата за избор од десната страна.</string>
|
||||
<string name="notification_actions_at_most_three">Можете да изберете најмногу три дејствија за да се прикажат во компактното известување!</string>
|
||||
<string name="no_live_streams">Нема стримови во живо</string>
|
||||
<string name="play_queue_audio_track">Аудио: %s</string>
|
||||
<string name="more_than_100_videos">100+ видеа</string>
|
||||
<string name="audio_track">Аудио трака</string>
|
||||
<string name="enqueued">Додадено во редослед</string>
|
||||
<string name="no_one_listening">Нема слушатели</string>
|
||||
<string name="error_report_notification_toast">Возникна грешка, погледнете го известувањето</string>
|
||||
<string name="error_report_open_issue_button_text">Пријави на GitHub</string>
|
||||
<plurals name="new_streams">
|
||||
<item quantity="one">%s нов стрим</item>
|
||||
<item quantity="other">%s нови стримови</item>
|
||||
</plurals>
|
||||
<string name="error_report_open_github_notice">Ве молиме, проверете дали веќе има дискусија за проблем како овој. Создадените дупликати ни одземаат од времето, коешто можеме да го посветиме на поправање на проблемот.</string>
|
||||
<string name="localization_changes_requires_app_restart">Јазикот ќе се смени откако апликацијата ќе биде рестартирана</string>
|
||||
<string name="enqueue_next_stream">Додај го во редослед следното</string>
|
||||
<string name="no_one_watching">Нема гледачи</string>
|
||||
<string name="done">Готово</string>
|
||||
<string name="no_dir_yet">Сѐ уште не е избрана папка за преземања, одберете ја стандардната папка за преземања сега</string>
|
||||
<string name="no_playlist_bookmarked_yet">Во моментов нема обележани плејлисти</string>
|
||||
<string name="default_kiosk_page_summary">Стандарден киоск</string>
|
||||
<string name="faq_description">Ако имате проблеми со користењето на апликацијата, не заборавајте да ги проверите одговориве на често поставувани прашања!</string>
|
||||
<string name="reset_all_settings">Ресетирањето на сите поставки ќе ги отфрли сите Ваши претпочитани поставки и ќе ја рестартира апликацијата.\n\nСигурни ли сте дека сакате да продолжите?</string>
|
||||
<string name="new_seek_duration_toast">Поради ограничувања на ExoPlayer, стапката на премотување е поставена на %d секунди</string>
|
||||
<string name="feed_group_show_only_ungrouped_subscriptions">Прикажи ги само негрупираните претплати</string>
|
||||
<string name="feed_load_error_terminated">Акаунтот на авторот е укинат.\nNewPipe нема да може да ги вчитува овие видеа во иднина.\nСакате ли да ја откажете претплатата на овој канал?</string>
|
||||
<string name="feed_update_threshold_summary">Време по последното ажурирање пред една претплата да се смета за застарена — %s</string>
|
||||
<string name="feed_use_dedicated_fetch_method_title">Извлекување од специјална новинска лента, кога е возможно</string>
|
||||
<string name="feed_hide_streams_title">Прикажи ги следните стримови</string>
|
||||
<string name="detail_sub_channel_thumbnail_view_description">Сликичка на аватарот на каналот</string>
|
||||
<string name="metadata_privacy_internal">Внатрешно</string>
|
||||
<string name="feed_use_dedicated_fetch_method_help_text">Сметате дека лентата со новости (новинската лента, анг. News Feed) се вчитува премногу бавно? Ако сметате така, можете да пробате да го вклучите режимот за брзо вчитување (може да го промените во поставките или со притискање на копчето подолу).\n \nNewPipe нуди два режима за вчитување на новинската лента:\n• Извлекување на целата страница на претплати, што е бавно но и целосно.\n• Користење на специјален клиент (анг. endpoint), овозможен од услугата, што е брзо, но обично е нецелосно.\n\nРазликата помеѓу двете е во тоа што брзиот режим најчесто дава нецелесни информации, како времетраењето и видот на видеото (не прави разлика помеѓу нормални видеа и преноси во живо) и можно е да врати помалку видеа.\n\nYouTube е пример за услуга која го овозможува овој брз режим со RSS новинската лента.\n\nНакратко, изборот се сведува на тоа што Вие претпочитате: брзина или точни информации.</string>
|
||||
<string name="no_appropriate_file_manager_message_android_10">Не е пронајден соодветен менаџер за фајлови за ова дејствие.\nВе молиме инталирајте менаџер за фајлови компатибилен со Storage Access Framework</string>
|
||||
<string name="description_select_note">Режимот на селектирање е вклучен. Сега можете да селектирате текст во описот на видеото. Имајте предвид дека страницата може да трепка и линковите може да не можат да се кликнат, додека сте во режим на селектирање.</string>
|
||||
<string name="streams_not_yet_supported_removed">Стримовите кои сѐ уште не се поддржани од програмата за преземање не се прикажани</string>
|
||||
<string name="selected_stream_external_player_not_supported">Озбраниот стрим не е поддржан од надворешни плеери</string>
|
||||
<string name="no_audio_streams_available_for_external_players">Нема достапни аудио стримови за надворешни плеери</string>
|
||||
<string name="settings_category_exoplayer_summary">Управувајте со некои поставки за ExoPlayer. За овие промени да стапат на сила, потребно е апликацијата да се рестартира</string>
|
||||
<string name="always_use_exoplayer_set_output_surface_workaround_summary">Ово заобиколување ги ослободува и повторно ги инстанцира видео кодеците кога ќе дојде до промена на површината, наместо директно да ја постави површината на кодекот. Веќе се користи од ExoPlayer на некои уреди со овој проблем. Оваа поставка има ефект само на Android 6 и понови верзии\n\nОвозможувањето на оваа поставка може да спречи грешки при емитување при префрлување на тековниот видео плеер или пуштање на цел екран</string>
|
||||
<string name="peertube_instance_add_fail">Инстанцата не може да биде потврдена</string>
|
||||
<string name="image_quality_summary">Изберете го квалитетот на сликите и дали воопшто да се вчитуваат слики, за да го намалите користењето на интернет и меморија. Промените го чистат кешот на сликите (анг. image cache), како и во меморијата, така и на дискот — %s</string>
|
||||
<string name="video_details_list_item">- %1$s: %2$s</string>
|
||||
<string name="none">Ништо</string>
|
||||
<string name="import_settings_vulnerable_format">Поставките во извезениот фајл кој се увезува користат ранлив формат кој повеќе не е поддржан од NewPipe 0.27.0. Уверете се дека извезениот фајл кој се увезува е од доверлив извор и претпочитајте во иднина да користите само износи добиени од NewPipe 0.27.0 или понова верзија. Поддршката за увезување поставки од овој ранлив формат наскоро ќе биде целосно укината и тогаш старите верзии на NewPipe повеќе нема да можат да увезуваат поставки од износи од новите верзии.</string>
|
||||
<string name="clear_queue_confirmation_title">Побарај потврда пред чистење на редоследот</string>
|
||||
<string name="left_gesture_control_title">Дејствие со гестикулација одлево</string>
|
||||
<string name="show_meta_info_summary">Исклучете за да ги скриете кутиите со метаподатоци со дополнителни информации за авторот на стримот, содржината на стримот или пребарување</string>
|
||||
<string name="prefer_descriptive_audio_summary">Ја избира аудио снимката со описи за луѓето го видна попреченост, доколку има таква</string>
|
||||
<string name="right_gesture_control_summary">Изберете гестикулација за десната половина од екранот на плеерот</string>
|
||||
<string name="start_main_player_fullscreen_summary">Видеата нема да започнат со емитување во миниплеерот, туку директно ќе се вклучат на цел екран, доколку автоматското ротирање е заклучено. Сѐ уште можете да добиете пристап до миниплеерот, кога ќе излезете од целиот екран</string>
|
||||
<string name="unsupported_url_dialog_message">URL адресата не може да биде распознаена. Да се отвори со друга апликација?</string>
|
||||
</resources>
|
@ -503,7 +503,6 @@
|
||||
<string name="image_quality_low">Mutu rendah</string>
|
||||
<string name="image_quality_medium">Mutu sederhana</string>
|
||||
<string name="image_quality_high">Mutu tinggi</string>
|
||||
<string name="share_playlist_with_titles_message">Kongsikan senarai main dengan butiran seperti nama senarai main dan tajuk video atau sebagai senarai ringkas URL video</string>
|
||||
<string name="share_playlist_with_titles">Kongsi dengan Tajuk</string>
|
||||
<string name="share_playlist_with_list">Kongsi senarai URL</string>
|
||||
<string name="show_more">Tunjukkan lagi</string>
|
||||
|
@ -804,7 +804,6 @@
|
||||
<string name="share_playlist_content_details">%1$s
|
||||
\n%2$s</string>
|
||||
<string name="share_playlist">Afspeellijst delen</string>
|
||||
<string name="share_playlist_with_titles_message">Deel afspeellijst met details zoals afspeellijstnaam en videotitels of als een eenvoudige lijst met video-URL\'s</string>
|
||||
<string name="video_details_list_item">- %1$s: %2$s</string>
|
||||
<plurals name="replies">
|
||||
<item quantity="one">%s reactie</item>
|
||||
@ -825,4 +824,5 @@
|
||||
\nWilt u dit inschakelen?</string>
|
||||
<string name="reset_settings_title">Instellingen resetten</string>
|
||||
<string name="import_settings_vulnerable_format">De instellingen in de export die wordt geïmporteerd, gebruiken een kwetsbaar formaat dat verouderd is sinds NewPipe 0.27.0. Zorg ervoor dat de export die wordt geïmporteerd afkomstig is van een vertrouwde bron, en geef er de voorkeur aan om in de toekomst alleen exporten te gebruiken die zijn verkregen van NewPipe 0.27.0 of nieuwer. Ondersteuning voor het importeren van instellingen in dit kwetsbare formaat zal binnenkort volledig worden verwijderd, en oude versies van NewPipe zullen dan geen exportinstellingen meer uit nieuwe versies kunnen importeren.</string>
|
||||
<string name="audio_track_type_secondary">secundair</string>
|
||||
</resources>
|
@ -804,7 +804,6 @@
|
||||
<string name="share_playlist_content_details">%1$s
|
||||
\n%2$s</string>
|
||||
<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>
|
||||
<string name="show_more">ଅଧିକ ଦର୍ଶାନ୍ତୁ</string>
|
||||
<string name="notification_actions_summary_android13">ଏହା ଉପରେ ଟ୍ୟାପ କରି ନିମ୍ନରେ ଦିଆଯାଇଥିବା ପ୍ରତ୍ୟେକ ବିଜ୍ଞପ୍ତି କାର୍ଯ୍ୟକୁ ସମ୍ପାଦନ କରନ୍ତୁ । ପ୍ରଥମ ତିନୋଟି କାର୍ଯ୍ୟ (ଖେଳ/ବିରତି, ପୂର୍ବବର୍ତ୍ତୀ ଏବଂ ପରବର୍ତ୍ତୀ) ତନ୍ତ୍ର ଦ୍ୱାରା ସେଟ କରାଯାଇଥାଏ ଏବଂ ଏହାକୁ ଇଚ୍ଛାରୂପଣ କରାଯାଇପାରିବ ନାହିଁ ।</string>
|
||||
|
@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="main_bg_subtitle">ਸ਼ੁਰੂ ਕਰਨ ਲਈ ਵੱਡਦਰਸ਼ੀ ਸ਼ੀਸ਼ੇ \'ਤੇ ਟੈਪ ਕਰੋ।</string>
|
||||
<string name="upload_date_text">%1$s ਨੂੰ ਪ੍ਰਕਾਸ਼ਿਤ ਕੀਤੀ ਗਈ</string>
|
||||
<string name="upload_date_text">%1$s ਨੂੰ ਪ੍ਰਕਾਸ਼ਿਤ ਹੋਇਆ</string>
|
||||
<string name="no_player_found">ਸਟ੍ਰੀਮਿੰਗ ਲਈ ਪਲੇਅਰ ਨਹੀਂ ਮਿਲਿਆ। ਕੀ ਤੁਸੀਂ VLC ਸਥਾਪਤ ਕਰਨਾ ਚਾਹੋਗੇ?</string>
|
||||
<string name="no_player_found_toast">ਸਟ੍ਰੀਮਿੰਗ ਲਈ ਪਲੇਅਰ ਨਹੀਂ ਮਿਲਿਆ (ਤੁਸੀਂ ਇਸਨੂੰ ਚਲਾਉਣ ਲਈ VLC ਪਲੇਅਰ ਇੰਸਟਾਲ ਕਰ ਸਕਦੇ ਹੋ)।</string>
|
||||
<string name="install">ਇੰਸਟਾਲ ਕਰੋ</string>
|
||||
@ -15,9 +15,9 @@
|
||||
<string name="settings">ਸੈਟਿੰਗਾਂ</string>
|
||||
<string name="did_you_mean">ਕੀ ਤੁਹਾਡਾ ਮਤਲਬ ਸੀ \"%1$s\"\?</string>
|
||||
<string name="share_dialog_title">ਦੇ ਨਾਲ ਸਾਂਝਾ ਕਰੋ</string>
|
||||
<string name="use_external_video_player_title">ਬਾਹਰੀ ਵੀਡੀਓ ਪਲੇਅਰ ਵਰਤੋ</string>
|
||||
<string name="use_external_video_player_title">ਬਾਹਰੀ ਵੀਡੀਓ ਪਲੇਅਰ ਦੀ ਵਰਤੋਂ ਕਰੋ</string>
|
||||
<string name="use_external_video_player_summary">ਕੁਝ ਰੈਜ਼ੋਲਿਊਸ਼ਨਾਂ \'ਤੇ ਆਵਾਜ਼ ਹਟ ਸਕਦੀ ਹੈ</string>
|
||||
<string name="use_external_audio_player_title">ਬਾਹਰੀ ਆਡੀਓ ਪਲੇਅਰ ਵਰਤੋ</string>
|
||||
<string name="use_external_audio_player_title">ਬਾਹਰੀ ਆਡੀਓ ਪਲੇਅਰ ਦੀ ਵਰਤੋਂ ਕਰੋ</string>
|
||||
<string name="subscribe_button_title">ਸਬਸਕ੍ਰਾਈਬ ਕਰੋ</string>
|
||||
<string name="subscribed_button_title">ਸਬਸਕ੍ਰਾਈਬ ਹੈ</string>
|
||||
<string name="channel_unsubscribed">ਚੈਨਲ ਅਨ-ਸਬਸਕ੍ਰਾਈਬ ਹੋਇਆ</string>
|
||||
@ -27,7 +27,7 @@
|
||||
<string name="tab_subscriptions">ਸਬਸਕ੍ਰਿਪਸ਼ਨਾਂ</string>
|
||||
<string name="tab_bookmarks">ਬੁੱਕਮਾਰਕ ਕੀਤੀਆਂ ਪਲੇਲਿਸਟਾਂ</string>
|
||||
<string name="fragment_feed_title">ਨਵਾਂ ਕੀ ਹੈ</string>
|
||||
<string name="controls_background_title">ਬੈਕਗ੍ਰਾਊਂਡ ਆਡੀਓ</string>
|
||||
<string name="controls_background_title">ਬੈਕਗ੍ਰਾਊਂਡ</string>
|
||||
<string name="controls_popup_title">ਪੌਪ-ਅਪ</string>
|
||||
<string name="controls_add_to_playlist_title">ਵਿੱਚ ਸ਼ਾਮਿਲ ਕਰੋ</string>
|
||||
<string name="download_path_title">ਵੀਡੀਓ ਲਈ ਡਾਊਨਲੋਡ ਫ਼ੋਲਡਰ</string>
|
||||
@ -52,7 +52,7 @@
|
||||
<string name="dark_theme_title">ਗੂੜ੍ਹਾ</string>
|
||||
<string name="black_theme_title">ਕਾਲ਼ਾ</string>
|
||||
<string name="popup_remember_size_pos_title">ਪੌਪ-ਅਪ ਦਾ ਆਕਾਰ ਅਤੇ ਸਥਿਤੀ ਯਾਦ ਰੱਖੋ</string>
|
||||
<string name="popup_remember_size_pos_summary">ਪੌਪ-ਅਪ ਦਾ ਆਖਰੀ ਅਕਾਰ ਅਤੇ ਸਥਿਤੀ ਯਾਦ ਰੱਖੋ</string>
|
||||
<string name="popup_remember_size_pos_summary">ਪੌਪ-ਅਪ ਦਾ ਆਖਰੀ ਆਕਾਰ ਅਤੇ ਸਥਿਤੀ ਯਾਦ ਰੱਖੋ</string>
|
||||
<string name="use_inexact_seek_title">ਤੇਜ਼ ਤੇ ਅਣਸਟੀਕ ਭਾਲ ਦੀ ਵਰਤੋਂ ਕਰੋ</string>
|
||||
<string name="use_inexact_seek_summary">ਅਣਸਟੀਕ ਭਾਲ ਨਾਲ ਪਲੇਅਰ ਘੱਟ ਸਟੀਕਤਾ ਦੇ ਪਰ ਅਧਿਕ ਤੇਜ਼ੀ ਨਾਲ ਵੀਡੀਓ ਸਥਿੱਤੀਆਂ ਦੀ ਤਲਾਸ਼ ਕਰ ਸਕਦਾ ਹੈ । ਇਸ ਨਾਲ ਅੱਗੇ-ਪਿੱਛੇ 5, 15 ਜਾਂ 25 ਸਕਿੰਟ ਲਿਜਾਣਾ ਕੰਮ ਨਹੀਂ ਕਰਦਾ ਹੈ</string>
|
||||
<string name="thumbnail_cache_wipe_complete_notice">ਚਿੱਤਰ ਕੈਸ਼ ਮਿਟਾਇਆ ਗਿਆ</string>
|
||||
@ -101,8 +101,8 @@
|
||||
<string name="notification_channel_name">ਨਿਊਪਾਈਪ ਨੋਟੀਫਿਕੇਸ਼ਨ</string>
|
||||
<string name="notification_channel_description">ਨਿਊਪਾਈਪ ਦੇ ਪਲੇਅਰ ਦੇ ਲਈ ਨੋਟੀਫਿਕੇਸ਼ਨ</string>
|
||||
<string name="unknown_content">[ਅਣਜਾਣ]</string>
|
||||
<string name="switch_to_background">ਬੈਕਗ੍ਰਾਊਂਡ ਵਿੱਚ ਚਲਾਓ</string>
|
||||
<string name="switch_to_popup">ਪੌਪ-ਅਪ ਵਿੱਚ ਚਲਾਓ</string>
|
||||
<string name="switch_to_background">ਬੈਕਗ੍ਰਾਊਂਡ ਮੋਡ ਵਿੱਚ ਚਲਾਓ</string>
|
||||
<string name="switch_to_popup">ਪੌਪ-ਅਪ ਮੋਡ ਵਿੱਚ ਚਲਾਓ</string>
|
||||
<string name="switch_to_main">ਮੇਨ ਤੇ ਚਲਾਓ</string>
|
||||
<string name="import_data_title">ਡਾਟਾਬੇਸ ਆਯਾਤ ਕਰੋ</string>
|
||||
<string name="export_data_title">ਡਾਟਾਬੇਸ ਨਿਰਯਾਤ ਕਰੋ</string>
|
||||
@ -316,7 +316,7 @@
|
||||
<string name="limit_data_usage_none_description">ਕੋਈ ਸੀਮਾ ਨਹੀਂ</string>
|
||||
<string name="limit_mobile_data_usage_title">ਮੋਬਾਈਲ ਡਾਟਾ ਦੀ ਵਰਤੋਂ ਕਰਦੇ ਸਮੇਂ ਰੈਜ਼ੋਲਿਊਸ਼ਨ ਨੂੰ ਸੀਮਿਤ ਕਰੋ</string>
|
||||
<string name="minimize_on_exit_title">ਐਪ ਬਦਲਦੇ ਸਮੇਂ ਉਸਨੂੰ ਮਿਨੀਮਾਈਜ਼ ਕਰੋ</string>
|
||||
<string name="minimize_on_exit_summary">ਮੇਨ ਵੀਡੀਓ ਪਲੇਅਰ ਤੋਂ ਦੂਜੇ ਐਪ \'ਤੇ ਜਾਣ ਵੇਲ਼ੇ ਕਾਰਵਾਈ — %s</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>
|
||||
@ -466,7 +466,7 @@
|
||||
<string name="no_app_to_open_intent">ਤੁਹਾਡੇ ਡਿਵਾਈਸ ਦੀ ਕੋਈ ਵੀ ਐਪ ਇਸ ਨੂੰ ਖੋਲ੍ਹ ਨਹੀਂ ਸਕਦੀ</string>
|
||||
<string name="chapters">ਚੈਪਟਰ</string>
|
||||
<string name="recent">ਹਾਲੀਆ</string>
|
||||
<string name="show_thumbnail_summary">ਥੰਮਨੇਲ ਨੂੰ ਤਾਲਾਬੱਧ ਸਕਰੀਨ ਦੇ ਪਿਛੋਕੜ ਅਤੇ ਨੋਟੀਫਿਕੇਸ਼ਨ ਦੋਵਾਂ ਲਈ ਵਰਤੋ</string>
|
||||
<string name="show_thumbnail_summary">ਥੰਮਨੇਲ ਨੂੰ ਲਾਕ ਸਕਰੀਨ ਦੇ ਬੈਕਗ੍ਰਾਊਂਡ ਅਤੇ ਨੋਟੀਫਿਕੇਸ਼ਨ ਦੋਵਾਂ ਲਈ ਵਰਤੋ</string>
|
||||
<string name="show_thumbnail_title">ਥੰਮਨੇਲ ਵਿਖਾਓ</string>
|
||||
<string name="playlist_page_summary">ਪਲੇਲਿਸਟ ਪੰਨਾ</string>
|
||||
<string name="video_detail_by">%s ਦੁਆਰਾ</string>
|
||||
@ -804,7 +804,6 @@
|
||||
<string name="share_playlist_content_details">%1$s
|
||||
\n%2$s</string>
|
||||
<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>
|
||||
|
@ -55,7 +55,7 @@
|
||||
<string name="error_snackbar_action">Zgłoś</string>
|
||||
<string name="what_device_headline">Informacje:</string>
|
||||
<string name="what_happened_headline">Co się stało:</string>
|
||||
<string name="info_labels">Co:\\nŻądanie:\\nJęzyk treści:\\nKraj treści:\\nJęzyk aplikacji:\\nUsługa:\\nCzas GMT:\\nPakiet:\\nWersja:\\nWersja systemu:</string>
|
||||
<string name="info_labels">Co:\\nŻądanie:\\nJęzyk treści:\\nKraj treści:\\nJęzyk aplikacji:\\nUsługa:\\nZnacznik czasu:\\nPakiet:\\nWersja:\\nWersja systemu:</string>
|
||||
<string name="your_comment">Twój komentarz (po angielsku):</string>
|
||||
<string name="error_details_headline">Szczegóły:</string>
|
||||
<string name="detail_thumbnail_view_description">Odtwarzane wideo, czas trwania:</string>
|
||||
@ -825,7 +825,6 @@
|
||||
<string name="share_playlist_content_details">%1$s
|
||||
\n%2$s</string>
|
||||
<string name="share_playlist">Udostępnij playlistę</string>
|
||||
<string name="share_playlist_with_titles_message">Udostępnij playlistę ze szczegółami, takimi jak nazwa playlisty i tytuły wideo, lub jako prostą listę adresów URL wideo.</string>
|
||||
<string name="video_details_list_item">– %1$s: %2$s</string>
|
||||
<plurals name="replies">
|
||||
<item quantity="one">%s odpowiedź</item>
|
||||
|
@ -174,7 +174,7 @@
|
||||
<string name="top_50">Top 50</string>
|
||||
<string name="new_and_hot">Novos e tendências</string>
|
||||
<string name="show_hold_to_append_title">Mostrar dica \"Segure para pôr na fila\"</string>
|
||||
<string name="show_hold_to_append_summary">Mostra dica ao tocar no botão segundo plano ou Popup em \"Detalhes:\" do vídeo</string>
|
||||
<string name="show_hold_to_append_summary">Mostra dica ao tocar no botão segundo plano ou popup em \"Detalhes:\" do vídeo</string>
|
||||
<string name="play_all">Reproduzir tudo</string>
|
||||
<string name="player_stream_failure">Não é possível reproduzir este vídeo</string>
|
||||
<string name="player_unrecoverable_failure">Ocorreu um erro irrecuperável na reprodução</string>
|
||||
@ -791,8 +791,9 @@
|
||||
<string name="channel_tab_livestreams">Ao vivo</string>
|
||||
<string name="image_quality_title">Qualidade da imagem</string>
|
||||
<string name="question_mark">\?</string>
|
||||
<string name="share_playlist_with_list">Compartilhar URL</string>
|
||||
<string name="share_playlist_with_titles">Compartilhar com título</string>
|
||||
<string name="share_playlist_with_list">Compartilhar URLs</string>
|
||||
<string name="share_playlist_with_titles">Compartilhar com títulos</string>
|
||||
<string name="share_playlist_as_youtube_temporary_playlist">Compartilhar como playlist temporária do YouTube</string>
|
||||
<string name="share_playlist_content_details">%1$s
|
||||
\n%2$s</string>
|
||||
<string name="toggle_screen_orientation">Alternar orientação da tela</string>
|
||||
@ -808,7 +809,6 @@
|
||||
<string name="forward">Avançar</string>
|
||||
<string name="rewind">Retroceder</string>
|
||||
<string name="replay">Repetir</string>
|
||||
<string name="share_playlist_with_titles_message">Compartilhar playlist com detalhes como o nome da playlist e títulos de vídeo ou como uma lista simples dos URL de vídeos</string>
|
||||
<string name="image_quality_medium">Qualidade média</string>
|
||||
<string name="metadata_uploader_avatars">Fotos de perfil do autor</string>
|
||||
<string name="video_details_list_item">- %1$s: %2$s</string>
|
||||
|
@ -804,7 +804,6 @@
|
||||
<string name="rewind">Recuar</string>
|
||||
<string name="replay">Repetição</string>
|
||||
<string name="feed_fetch_channel_tabs_summary">Separadores a obter ao atualizar o feed. Esta opção não tem efeito se um canal for atualizado utilizando o modo rápido.</string>
|
||||
<string name="share_playlist_with_titles_message">Partilhe a lista de reprodução com detalhes como o nome da lista de reprodução e os títulos dos vídeos ou como uma simples lista de URLs de vídeos</string>
|
||||
<string name="image_quality_medium">Média qualidade</string>
|
||||
<string name="metadata_uploader_avatars">Avatar dos publicadores</string>
|
||||
<string name="metadata_banners">Bandeiras</string>
|
||||
@ -839,4 +838,5 @@
|
||||
<string name="reset_settings_title">Repor definições</string>
|
||||
<string name="error_insufficient_storage">Não há espaço suficiente no aparelho</string>
|
||||
<string name="import_settings_vulnerable_format">As configurações na exportação a serem importadas usam um formato vulnerável depreciado desde NewPipe 0.27.0. Certifique-se de que a exportação que é importada é de uma fonte confiável e prefira usar apenas as exportações obtidas do NewPipe 0.27.0 ou mais recentes no futuro. O suporte para importar configurações neste formato vulnerável será removido em breve completamente e, em seguida, versões antigas do NewPipe não serão capazes de importar configurações de exportações de novas versões.</string>
|
||||
<string name="audio_track_type_secondary">secundário</string>
|
||||
</resources>
|
@ -25,7 +25,7 @@
|
||||
<string name="default_audio_format_title">Formato padrão de áudio</string>
|
||||
<string name="download_dialog_title">Descarregar</string>
|
||||
<string name="show_next_and_similar_title">Mostrar vídeos \'Seguintes\' e \'Semelhantes\'</string>
|
||||
<string name="unsupported_url">URL não suportado</string>
|
||||
<string name="unsupported_url">URL não suportada</string>
|
||||
<string name="content_language_title">Idioma padrão para conteúdo</string>
|
||||
<string name="settings_category_video_audio_title">Vídeo e áudio</string>
|
||||
<string name="detail_thumbnail_view_description">Reproduzir vídeo, duração:</string>
|
||||
@ -281,7 +281,7 @@
|
||||
<string name="clear_views_history_title">Limpar histórico de visualizações</string>
|
||||
<string name="auto_queue_summary">Continuar (sem repetição) a fila de reprodução anexando um vídeo relacionado</string>
|
||||
<string name="show_hold_to_append_title">Mostrar dica \"Toque longo para colocar na fila\"</string>
|
||||
<string name="show_hold_to_append_summary">Mostrar dica ao premir em segundo plano ou no botão \"Detalhes\" da janela popup</string>
|
||||
<string name="show_hold_to_append_summary">Mostrar dica ao premir em segundo plano ou no botão \"Detalhes\" da janela popup:</string>
|
||||
<string name="channels">Canais</string>
|
||||
<string name="playlists">Listas de reprodução</string>
|
||||
<string name="tracks">Faixas</string>
|
||||
@ -551,7 +551,7 @@
|
||||
<string name="never">Nunca</string>
|
||||
<string name="notification_action_buffering">A carregar</string>
|
||||
<string name="clear_queue_confirmation_description">A fila de reprodução atual será substituída</string>
|
||||
<string name="unsupported_url_dialog_message">URL não reconhecido. Abrir com outra aplicação\?</string>
|
||||
<string name="unsupported_url_dialog_message">URL não reconhecida. Abrir com outra aplicação?</string>
|
||||
<string name="auto_queue_toggle">Colocar na fila automaticamente</string>
|
||||
<string name="notification_action_shuffle">Baralhar</string>
|
||||
<string name="wifi_only">Apenas em Wi-Fi</string>
|
||||
@ -817,7 +817,6 @@
|
||||
<string name="channel_tab_channels">Canais</string>
|
||||
<string name="previous_stream">Vídeo anterior</string>
|
||||
<string name="channel_tab_livestreams">Direto</string>
|
||||
<string name="share_playlist_with_titles_message">Partilhe a lista de reprodução com detalhes como o nome da lista de reprodução e os títulos dos vídeos ou como uma simples lista de URLs de vídeos</string>
|
||||
<string name="image_quality_summary">Escolha a qualidade das imagens e se pretende carregar imagens, para reduzir a utilização de dados e de memória. As alterações limpam a cache de imagens na memória e no disco - %s</string>
|
||||
<string name="show_more">Mostrar mais</string>
|
||||
<plurals name="replies">
|
||||
@ -839,5 +838,5 @@
|
||||
<string name="auto_update_check_description">O NewPipe pode verificar automaticamente se há novas versões de tempos em tempos e notificá-lo quando elas estiverem disponíveis.
|
||||
\nDeseja ativar essa opção?</string>
|
||||
<string name="import_settings_vulnerable_format">As configurações na exportação a serem importadas usam um formato vulnerável depreciado desde NewPipe 0.27.0. Certifique-se de que a exportação que é importada é de uma fonte confiável e prefira usar apenas as exportações obtidas do NewPipe 0.27.0 ou mais recentes no futuro. O suporte para importar configurações neste formato vulnerável será removido em breve completamente e, em seguida, versões antigas do NewPipe não serão capazes de importar configurações de exportações de novas versões.</string>
|
||||
<string name="audio_track_type_secondary">secundario</string>
|
||||
<string name="audio_track_type_secondary">secundário</string>
|
||||
</resources>
|
@ -791,7 +791,6 @@
|
||||
<string name="feed_fetch_channel_tabs_summary">File ce vor fi preluate când se actualizează fluxul. Această opțiune nu are niciun efect dacă un canal este actualizat folosind modul rapid.</string>
|
||||
<string name="prefer_descriptive_audio_summary">Selectați o coloană sonoră cu descrieri pentru persoane cu deficiențe vizuale, dacă este disponibilă</string>
|
||||
<string name="left_gesture_control_title">Acțiunea gestului din stânga</string>
|
||||
<string name="share_playlist_with_titles_message">Distribuiți playlistul cu detalii precum numele playlistului și titlurile videourilor sau ca o simplă listă de URL-uri a videourilor</string>
|
||||
<string name="image_quality_medium">Calitate medie</string>
|
||||
<string name="prefer_descriptive_audio_title">Preferați audioul descriptiv</string>
|
||||
<string name="progressive_load_interval_summary">Modificați dimensiunea intervalului de încărcare pentru conținuturi progresive (în prezent %s). O valoare mai mică poate accelera încărcarea lor inițială</string>
|
||||
@ -839,4 +838,5 @@
|
||||
\n
|
||||
\nSigur doriți să continuați?</string>
|
||||
<string name="import_settings_vulnerable_format">Setările din exportul importat folosesc un format vulnerabil care a fost depreciat de la NewPipe 0.27.0. Asigurați-vă că exportul care este importat este dintr-o sursă de încredere și preferați să utilizați numai exporturi obținute din NewPipe 0.27.0 sau mai nou în viitor. Suportul pentru importul setărilor în acest format vulnerabil va fi în curând eliminat complet, iar versiunile vechi ale NewPipe nu vor mai putea importa setările exporturilor din versiunile noi.</string>
|
||||
<string name="audio_track_type_secondary">secundar</string>
|
||||
</resources>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user